FreeTube/src/renderer/components/subscriptions-videos/subscriptions-videos.js

390 lines
12 KiB
JavaScript

import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
import { setPublishedTimestampsInvidious, copyToClipboard, showToast } from '../../helpers/utils'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { getLocalChannelVideos } from '../../helpers/api/local'
import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
export default defineComponent({
name: 'SubscriptionsVideos',
components: {
'subscriptions-tab-ui': SubscriptionsTabUI
},
data: function () {
return {
isLoading: false,
videoList: [],
errorChannels: [],
attemptedFetch: false,
}
},
computed: {
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
useRssFeeds: function () {
return this.$store.getters.getUseRssFeeds
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeProfileId: function () {
return this.activeProfile._id
},
cacheEntriesForAllActiveProfileChannels() {
const entries = []
this.activeSubscriptionList.forEach((channel) => {
const cacheEntry = this.$store.getters.getVideoCacheByChannel(channel.id)
if (cacheEntry == null) { return }
entries.push(cacheEntry)
})
return entries
},
videoCacheForAllActiveProfileChannelsPresent() {
if (this.cacheEntriesForAllActiveProfileChannels.length === 0) { return false }
if (this.cacheEntriesForAllActiveProfileChannels.length < this.activeSubscriptionList.length) { return false }
return this.cacheEntriesForAllActiveProfileChannels.every((cacheEntry) => {
return cacheEntry.videos != null
})
},
activeSubscriptionList: function () {
return this.activeProfile.subscriptions
},
fetchSubscriptionsAutomatically: function() {
return this.$store.getters.getFetchSubscriptionsAutomatically
},
},
watch: {
activeProfile: async function (_) {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
},
mounted: async function () {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
methods: {
loadVideosFromCacheSometimes() {
// This method is called on view visible
if (this.videoCacheForAllActiveProfileChannelsPresent) {
this.loadVideosFromCacheForAllActiveProfileChannels()
return
}
this.maybeLoadVideosForSubscriptionsFromRemote()
},
async loadVideosFromCacheForAllActiveProfileChannels() {
const videoList = []
this.activeSubscriptionList.forEach((channel) => {
const channelCacheEntry = this.$store.getters.getVideoCacheByChannel(channel.id)
videoList.push(...channelCacheEntry.videos)
})
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
},
loadVideosForSubscriptionsFromRemote: async function () {
if (this.activeSubscriptionList.length === 0) {
this.isLoading = false
this.videoList = []
return
}
const channelsToLoadFromRemote = this.activeSubscriptionList
const videoList = []
let channelCount = 0
this.isLoading = true
let useRss = this.useRssFeeds
if (channelsToLoadFromRemote.length >= 125 && !useRss) {
showToast(
this.$t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'),
10000
)
useRss = true
}
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
this.attemptedFetch = true
this.errorChannels = []
const subscriptionUpdates = []
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
let name, thumbnailUrl
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
if (useRss) {
({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousRSS(channel))
} else {
({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousScraper(channel))
}
} else {
if (useRss) {
({ videos, name, thumbnailUrl } = await this.getChannelVideosLocalRSS(channel))
} else {
({ videos, name, thumbnailUrl } = await this.getChannelVideosLocalScraper(channel))
}
}
channelCount++
const percentageComplete = (channelCount / channelsToLoadFromRemote.length) * 100
this.setProgressBarPercentage(percentageComplete)
this.updateSubscriptionVideosCacheByChannel({
channelId: channel.id,
videos: videos,
})
if (name || thumbnailUrl) {
subscriptionUpdates.push({
channelId: channel.id,
channelName: name,
channelThumbnailUrl: thumbnailUrl
})
}
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
},
maybeLoadVideosForSubscriptionsFromRemote: async function () {
if (this.fetchSubscriptionsAutomatically) {
// `this.isLoading = false` is called inside `loadVideosForSubscriptionsFromRemote` when needed
await this.loadVideosForSubscriptionsFromRemote()
} else {
this.videoList = []
this.attemptedFetch = false
this.isLoading = false
}
},
getChannelVideosLocalScraper: async function (channel, failedAttempts = 0) {
try {
const result = await getLocalChannelVideos(channel.id)
if (result === null) {
this.errorChannels.push(channel)
return {
videos: []
}
}
return result
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
switch (failedAttempts) {
case 0:
return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return await this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
} else {
return {
videos: []
}
}
case 2:
return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
default:
return {
videos: []
}
}
}
},
getChannelVideosLocalRSS: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UULF')
const feedUrl = `https://www.youtube.com/feeds/videos.xml?playlist_id=${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 404) {
// playlists don't exist if the channel was terminated but also if it doesn't have the tab,
// so we need to check the channel feed too before deciding it errored, as that only 404s if the channel was terminated
const response2 = await fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${channel.id}`, {
method: 'HEAD'
})
if (response2.status === 404) {
this.errorChannels.push(channel)
}
return {
videos: []
}
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
return this.getChannelVideosLocalScraper(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)
} else {
return {
videos: []
}
}
case 2:
return this.getChannelVideosLocalScraper(channel, failedAttempts + 1)
default:
return {
videos: []
}
}
}
},
getChannelVideosInvidiousScraper: function (channel, failedAttempts = 0) {
return new Promise((resolve, reject) => {
const subscriptionsPayload = {
resource: 'channels/latest',
id: channel.id,
params: {}
}
invidiousAPICall(subscriptionsPayload).then((result) => {
setPublishedTimestampsInvidious(result.videos)
let name
if (result.videos.length > 0) {
name = result.videos.find(video => video.type === 'video' && video.author).author
}
resolve({
name,
videos: result.videos
})
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err.responseText}`, 10000, () => {
copyToClipboard(err.responseText)
})
switch (failedAttempts) {
case 0:
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
break
case 1:
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
} else {
resolve({
videos: []
})
}
break
case 2:
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
break
default:
resolve({
videos: []
})
}
})
})
},
getChannelVideosInvidiousRSS: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UULF')
const feedUrl = `${this.currentInvidiousInstance}/feed/playlist/${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 500 || response.status === 404) {
this.errorChannels.push(channel)
return {
videos: []
}
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
case 1:
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
return this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
} else {
return {
videos: []
}
}
case 2:
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
default:
return {
videos: []
}
}
}
},
...mapActions([
'batchUpdateSubscriptionDetails',
'updateShowProgressBar',
'updateSubscriptionVideosCacheByChannel',
]),
...mapMutations([
'setProgressBarPercentage'
])
}
})