FreeTube/src/renderer/views/Channel/Channel.js

727 lines
22 KiB
JavaScript

import Vue from 'vue'
import { mapActions } from 'vuex'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import FtInput from '../../components/ft-input/ft-input.vue'
import FtSelect from '../../components/ft-select/ft-select.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtChannelBubble from '../../components/ft-channel-bubble/ft-channel-bubble.vue'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import ytch from 'yt-channel-info'
import autolinker from 'autolinker'
export default Vue.extend({
name: 'Search',
components: {
'ft-card': FtCard,
'ft-button': FtButton,
'ft-input': FtInput,
'ft-select': FtSelect,
'ft-flex-box': FtFlexBox,
'ft-channel-bubble': FtChannelBubble,
'ft-loader': FtLoader,
'ft-element-list': FtElementList
},
data: function () {
return {
isLoading: false,
isElementListLoading: false,
currentTab: 'videos',
id: '',
channelName: '',
bannerUrl: '',
thumbnailUrl: '',
subCount: 0,
latestVideosPage: 2,
searchPage: 2,
videoContinuationString: '',
playlistContinuationString: '',
searchContinuationString: '',
channelDescription: '',
videoSortBy: 'newest',
playlistSortBy: 'last',
lastSearchQuery: '',
relatedChannels: [],
latestVideos: [],
latestPlaylists: [],
searchResults: [],
shownElementList: [],
apiUsed: '',
videoSelectValues: [
'newest',
'oldest',
'popular'
],
playlistSelectValues: [
'last',
'newest'
]
}
},
computed: {
usingElectron: function () {
return this.$store.getters.getUsingElectron
},
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
sessionSearchHistory: function () {
return this.$store.getters.getSessionSearchHistory
},
profileList: function () {
return this.$store.getters.getProfileList
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
isSubscribed: function () {
const subIndex = this.profileList[this.activeProfile].subscriptions.findIndex((channel) => {
return channel.id === this.id
})
if (subIndex === -1) {
return false
} else {
return true
}
},
subscribedText: function () {
if (this.isSubscribed) {
return this.$t('Channel.Unsubscribe').toUpperCase()
} else {
return this.$t('Channel.Subscribe').toUpperCase()
}
},
videoSelectNames: function () {
return [
this.$t('Channel.Videos.Sort Types.Newest'),
this.$t('Channel.Videos.Sort Types.Oldest'),
this.$t('Channel.Videos.Sort Types.Most Popular')
]
},
playlistSelectNames: function () {
return [
this.$t('Channel.Playlists.Sort Types.Last Video Added'),
this.$t('Channel.Playlists.Sort Types.Newest')
]
},
formattedSubCount: function () {
if (this.hideChannelSubscriptions) {
return null
}
return this.subCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
},
showFetchMoreButton: function () {
switch (this.currentTab) {
case 'videos':
if (this.apiUsed === 'invidious' || (this.videoContinuationString !== '' && this.videoContinuationString !== null)) {
return true
}
break
case 'playlists':
if (this.playlistContinuationString !== '' && this.playlistContinuationString !== null) {
return true
}
break
case 'search':
if (this.searchContinuationString !== '' && this.searchContinuationString !== null) {
return true
}
break
}
return false
},
hideChannelSubscriptions: function () {
return this.$store.getters.getHideChannelSubscriptions
}
},
watch: {
$route() {
// react to route changes...
this.id = this.$route.params.id
this.currentTab = this.$route.params.currentTab ?? 'videos'
this.latestVideosPage = 2
this.searchPage = 2
this.relatedChannels = []
this.latestVideos = []
this.latestPlaylists = []
this.searchResults = []
this.shownElementList = []
this.apiUsed = ''
this.isLoading = true
if (!this.usingElectron) {
this.getVideoInformationInvidious()
} else {
switch (this.backendPreference) {
case 'local':
this.getChannelInfoLocal()
this.getChannelVideosLocal()
this.getPlaylistsLocal()
break
case 'invidious':
this.getChannelInfoInvidious()
this.getPlaylistsInvidious()
break
}
}
},
videoSortBy () {
this.isElementListLoading = true
this.latestVideos = []
switch (this.apiUsed) {
case 'local':
this.getChannelVideosLocal()
break
case 'invidious':
this.latestVideosPage = 1
this.channelInvidiousNextPage()
break
default:
this.getChannelVideosLocal()
}
},
playlistSortBy () {
this.isElementListLoading = true
this.latestPlaylists = []
this.playlistContinuationString = ''
switch (this.apiUsed) {
case 'local':
this.getPlaylistsLocal()
break
case 'invidious':
this.channelInvidiousNextPage()
break
default:
this.getPlaylistsLocal()
}
}
},
mounted: function () {
this.id = this.$route.params.id
this.currentTab = this.$route.params.currentTab ?? 'videos'
this.isLoading = true
if (!this.usingElectron) {
this.getVideoInformationInvidious()
} else {
switch (this.backendPreference) {
case 'local':
this.getChannelInfoLocal()
this.getChannelVideosLocal()
this.getPlaylistsLocal()
break
case 'invidious':
this.getChannelInfoInvidious()
this.getPlaylistsInvidious()
break
}
}
},
methods: {
goToChannel: function (id) {
this.$router.push({ path: `/channel/${id}` })
},
getChannelInfoLocal: function () {
this.apiUsed = 'local'
ytch.getChannelInfo(this.id).then((response) => {
this.id = response.authorId
this.channelName = response.author
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
if (this.hideChannelSubscriptions || response.subscriberCount === 0) {
this.subCount = null
} else {
this.subCount = response.subscriberCount.toFixed(0)
}
this.thumbnailUrl = response.authorThumbnails[2].url
this.channelDescription = autolinker.link(response.description)
this.relatedChannels = response.relatedChannels.items
this.relatedChannels.forEach(relatedChannel => {
relatedChannel.authorThumbnails.map(thumbnail => {
if (!thumbnail.url.includes('https')) {
thumbnail.url = `https:${thumbnail.url}`
}
return thumbnail
})
})
if (response.authorBanners !== null) {
const bannerUrl = response.authorBanners[response.authorBanners.length - 1].url
if (!bannerUrl.includes('https')) {
this.bannerUrl = `https://${bannerUrl}`
} else {
this.bannerUrl = bannerUrl
}
} else {
this.bannerUrl = null
}
this.isLoading = false
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
if (this.backendPreference === 'local' && this.backendFallback) {
this.showToast({
message: this.$t('Falling back to Invidious API')
})
this.getChannelInfoInvidious()
} else {
this.isLoading = false
}
})
},
getChannelVideosLocal: function () {
this.isElementListLoading = true
ytch.getChannelVideos(this.id, this.videoSortBy).then((response) => {
this.latestVideos = response.items
this.videoContinuationString = response.continuation
this.isElementListLoading = false
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
if (this.backendPreference === 'local' && this.backendFallback) {
this.showToast({
message: this.$t('Falling back to Invidious API')
})
this.getChannelInfoInvidious()
} else {
this.isLoading = false
}
})
},
channelLocalNextPage: function () {
ytch.getChannelVideosMore(this.videoContinuationString).then((response) => {
this.latestVideos = this.latestVideos.concat(response.items)
this.videoContinuationString = response.continuation
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
})
},
getChannelInfoInvidious: function () {
this.isLoading = true
this.apiUsed = 'invidious'
this.invidiousGetChannelInfo(this.id).then((response) => {
console.log(response)
this.channelName = response.author
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
this.id = response.authorId
if (this.hideChannelSubscriptions) {
this.subCount = null
} else {
this.subCount = response.subCount
}
this.thumbnailUrl = response.authorThumbnails[3].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
this.channelDescription = autolinker.link(response.description)
this.relatedChannels = response.relatedChannels.map((channel) => {
channel.authorThumbnails[channel.authorThumbnails.length - 1].url = channel.authorThumbnails[channel.authorThumbnails.length - 1].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
return channel
})
this.latestVideos = response.latestVideos
if (typeof (response.authorBanners) !== 'undefined') {
this.bannerUrl = response.authorBanners[0].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
}
this.isLoading = false
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err.responseJSON.error}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
this.isLoading = false
})
},
channelInvidiousNextPage: function () {
const payload = {
resource: 'channels/videos',
id: this.id,
params: {
sort_by: this.videoSortBy,
page: this.latestVideosPage
}
}
this.invidiousAPICall(payload).then((response) => {
this.latestVideos = this.latestVideos.concat(response)
this.latestVideosPage++
this.isElementListLoading = false
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
})
},
getPlaylistsLocal: function () {
ytch.getChannelPlaylistInfo(this.id, this.playlistSortBy).then((response) => {
console.log(response)
this.latestPlaylists = response.items.map((item) => {
item.proxyThumbnail = false
return item
})
this.playlistContinuationString = response.continuation
this.isElementListLoading = false
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
if (this.backendPreference === 'local' && this.backendFallback) {
this.showToast({
message: this.$t('Falling back to Invidious API')
})
this.getPlaylistsInvidious()
} else {
this.isLoading = false
}
})
},
getPlaylistsLocalMore: function () {
ytch.getChannelPlaylistsMore(this.playlistContinuationString).then((response) => {
console.log(response)
this.latestPlaylists = this.latestPlaylists.concat(response.items)
this.playlistContinuationString = response.continuation
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
})
},
getPlaylistsInvidious: function () {
if (this.playlistContinuationString === null) {
console.log('There are no more playlists available for this channel')
return
}
const payload = {
resource: 'channels/playlists',
id: this.id,
params: {
sort_by: this.playlistSortBy
}
}
if (this.playlistContinuationString) {
payload.params.continuation = this.playlistContinuationString
}
this.invidiousAPICall(payload).then((response) => {
this.playlistContinuationString = response.continuation
this.latestPlaylists = this.latestPlaylists.concat(response.playlists)
this.isElementListLoading = false
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err.responseJSON.error}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err.responseJSON.error)
}
})
if (this.backendPreference === 'invidious' && this.backendFallback) {
this.showToast({
message: this.$t('Falling back to Local API')
})
this.getPlaylistsLocal()
} else {
this.isLoading = false
}
})
},
handleSubscription: function () {
const currentProfile = JSON.parse(JSON.stringify(this.profileList[this.activeProfile]))
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
if (this.isSubscribed) {
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
return channel.id !== this.id
})
this.updateProfile(currentProfile)
this.showToast({
message: this.$t('Channel.Channel has been removed from your subscriptions')
})
if (this.activeProfile === 0) {
// Check if a subscription exists in a different profile.
// Remove from there as well.
let duplicateSubscriptions = 0
this.profileList.forEach((profile) => {
if (profile._id === 'allChannels') {
return
}
const parsedProfile = JSON.parse(JSON.stringify(profile))
const index = parsedProfile.subscriptions.findIndex((channel) => {
return channel.id === this.id
})
if (index !== -1) {
duplicateSubscriptions++
parsedProfile.subscriptions = parsedProfile.subscriptions.filter((x) => {
return x.id !== this.id
})
this.updateProfile(parsedProfile)
}
})
if (duplicateSubscriptions > 0) {
const message = this.$t('Channel.Removed subscription from $ other channel(s)')
this.showToast({
message: message.replace('$', duplicateSubscriptions)
})
}
}
} else {
const subscription = {
id: this.id,
name: this.channelName,
thumbnail: this.thumbnailUrl
}
currentProfile.subscriptions.push(subscription)
this.updateProfile(currentProfile)
this.showToast({
message: this.$t('Channel.Added channel to your subscriptions')
})
if (this.activeProfile !== 0) {
const index = primaryProfile.subscriptions.findIndex((channel) => {
return channel.id === this.id
})
if (index === -1) {
primaryProfile.subscriptions.push(subscription)
this.updateProfile(primaryProfile)
}
}
}
},
handleFetchMore: function () {
switch (this.currentTab) {
case 'videos':
switch (this.apiUsed) {
case 'local':
this.channelLocalNextPage()
break
case 'invidious':
this.channelInvidiousNextPage()
break
}
break
case 'playlists':
switch (this.apiUsed) {
case 'local':
this.getPlaylistsLocalMore()
break
case 'invidious':
this.getPlaylistsInvidious()
break
}
break
case 'search':
switch (this.apiUsed) {
case 'local':
this.searchChannelLocal()
break
case 'invidious':
this.searchChannelInvidious()
break
}
break
}
},
changeTab: function (tab) {
this.currentTab = tab
},
newSearch: function (query) {
this.lastSearchQuery = query
this.searchContinuationString = ''
this.isElementListLoading = true
this.searchPage = 1
this.searchResults = []
this.changeTab('search')
switch (this.apiUsed) {
case 'local':
this.searchChannelLocal()
break
case 'invidious':
this.searchChannelInvidious()
break
}
},
searchChannelLocal: function () {
if (this.searchContinuationString === '') {
ytch.searchChannel(this.id, this.lastSearchQuery).then((response) => {
console.log(response)
this.searchResults = response.items
this.isElementListLoading = false
this.searchContinuationString = response.continuation
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
if (this.backendPreference === 'local' && this.backendFallback) {
this.showToast({
message: this.$t('Falling back to Invidious API')
})
this.searchChannelInvidious()
} else {
this.isLoading = false
}
})
} else {
ytch.searchChannelMore(this.searchContinuationString).then((response) => {
console.log(response)
this.searchResults = this.searchResults.concat(response.items)
this.isElementListLoading = false
this.searchContinuationString = response.continuation
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
})
}
},
searchChannelInvidious: function () {
const payload = {
resource: 'channels/search',
id: this.id,
params: {
q: this.lastSearchQuery,
page: this.searchPage
}
}
this.invidiousAPICall(payload).then((response) => {
this.searchResults = this.searchResults.concat(response)
this.isElementListLoading = false
this.searchPage++
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
if (this.backendPreference === 'invidious' && this.backendFallback) {
this.showToast({
message: this.$t('Falling back to Local API')
})
this.searchChannelLocal()
} else {
this.isLoading = false
}
})
},
...mapActions([
'showToast',
'updateProfile',
'invidiousGetChannelInfo',
'invidiousAPICall'
])
}
})