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

347 lines
11 KiB
JavaScript

import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { getLocalChannelLiveStreams } from '../../helpers/api/local'
import { addPublishedDatesInvidious, addPublishedDatesLocal, parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
export default defineComponent({
name: 'SubscriptionsLive',
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.getLiveCacheByChannel(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.getLiveCacheByChannel(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 videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
if (useRss) {
videos = await this.getChannelLiveInvidiousRSS(channel)
} else {
videos = await this.getChannelLiveInvidious(channel)
}
} else {
if (useRss) {
videos = await this.getChannelLiveLocalRSS(channel)
} else {
videos = await this.getChannelLiveLocal(channel)
}
}
channelCount++
const percentageComplete = (channelCount / channelsToLoadFromRemote.length) * 100
this.setProgressBarPercentage(percentageComplete)
this.updateSubscriptionLiveCacheByChannel({
channelId: channel.id,
videos: videos,
})
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
},
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
}
},
getChannelLiveLocal: async function (channel, failedAttempts = 0) {
try {
const entries = await getLocalChannelLiveStreams(channel.id)
if (entries === null) {
this.errorChannels.push(channel)
return []
}
addPublishedDatesLocal(entries)
return entries
} 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.getChannelLiveLocalRSS(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return await this.getChannelLiveInvidious(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return await this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
default:
return []
}
}
},
getChannelLiveLocalRSS: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UULV')
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 []
}
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.getChannelLiveLocal(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return this.getChannelLiveLocal(channel, failedAttempts + 1)
default:
return []
}
}
},
getChannelLiveInvidious: function (channel, failedAttempts = 0) {
return new Promise((resolve, reject) => {
const subscriptionsPayload = {
resource: 'channels',
id: channel.id,
subResource: 'streams',
params: {}
}
invidiousAPICall(subscriptionsPayload).then((result) => {
const videos = result.videos.filter(e => e.type === 'video')
addPublishedDatesInvidious(videos)
resolve(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.getChannelLiveInvidiousRSS(channel, failedAttempts + 1))
break
case 1:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
resolve(this.getChannelLiveLocal(channel, failedAttempts + 1))
} else {
resolve([])
}
break
case 2:
resolve(this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1))
break
default:
resolve([])
}
})
})
},
getChannelLiveInvidiousRSS: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UULV')
const feedUrl = `${this.currentInvidiousInstance}/feed/playlist/${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 500 || response.status === 404) {
return []
}
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.getChannelLiveInvidious(channel, failedAttempts + 1)
case 1:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
return this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return this.getChannelLiveInvidious(channel, failedAttempts + 1)
default:
return []
}
}
},
...mapActions([
'updateShowProgressBar',
'updateSubscriptionLiveCacheByChannel',
]),
...mapMutations([
'setProgressBarPercentage'
])
}
})