import Vue from 'vue' import { mapActions, mapMutations } from 'vuex' import FtLoader from '../../components/ft-loader/ft-loader.vue' import FtCard from '../../components/ft-card/ft-card.vue' import FtButton from '../../components/ft-button/ft-button.vue' import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue' import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue' import FtElementList from '../../components/ft-element-list/ft-element-list.vue' import ytch from 'yt-channel-info' import Parser from 'rss-parser' import { MAIN_PROFILE_ID } from '../../../constants' export default Vue.extend({ name: 'Subscriptions', components: { 'ft-loader': FtLoader, 'ft-card': FtCard, 'ft-button': FtButton, 'ft-icon-button': FtIconButton, 'ft-flex-box': FtFlexBox, 'ft-element-list': FtElementList }, data: function () { return { isLoading: false, dataLimit: 100, videoList: [] } }, 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 }, hideWatchedSubs: function () { return this.$store.getters.getHideWatchedSubs }, useRssFeeds: function () { return this.$store.getters.getUseRssFeeds }, profileList: function () { return this.$store.getters.getProfileList }, activeVideoList: function () { if (this.videoList.length < this.dataLimit) { return this.videoList } else { return this.videoList.slice(0, this.dataLimit) } }, activeProfile: function () { return this.$store.getters.getActiveProfile }, profileSubscriptions: function () { return this.$store.getters.getProfileSubscriptions }, allSubscriptionsList: function () { return this.$store.getters.getAllSubscriptionsList }, historyCache: function () { return this.$store.getters.getHistoryCache }, activeSubscriptionList: function () { return this.activeProfile.subscriptions } }, watch: { activeProfile: async function (_) { this.getProfileSubscriptions() } }, mounted: async function () { this.isLoading = true const dataLimit = sessionStorage.getItem('subscriptionLimit') if (dataLimit !== null) { this.dataLimit = dataLimit } if (this.profileSubscriptions.videoList.length !== 0) { if (this.profileSubscriptions.activeProfile === this.activeProfile._id) { const subscriptionList = JSON.parse(JSON.stringify(this.profileSubscriptions)) if (this.hideWatchedSubs) { this.videoList = await Promise.all(subscriptionList.videoList.filter((video) => { const historyIndex = this.historyCache.findIndex((x) => { return x.videoId === video.videoId }) return historyIndex === -1 })) } else { this.videoList = subscriptionList.videoList } } else { this.getProfileSubscriptions() } this.isLoading = false } else { setTimeout(async () => { this.getSubscriptions() }, 300) } }, methods: { getSubscriptions: function () { if (this.activeSubscriptionList.length === 0) { this.isLoading = false this.videoList = [] return } let useRss = this.useRssFeeds if (this.activeSubscriptionList.length >= 125 && !useRss) { this.showToast({ message: this.$t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'), time: 10000 }) useRss = true } this.isLoading = true this.updateShowProgressBar(true) this.setProgressBarPercentage(0) let videoList = [] let channelCount = 0 this.activeSubscriptionList.forEach(async (channel) => { let videos = [] if (!this.usingElectron || this.backendPreference === 'invidious') { if (useRss) { videos = await this.getChannelVideosInvidiousRSS(channel) } else { videos = await this.getChannelVideosInvidiousScraper(channel) } } else { if (useRss) { videos = await this.getChannelVideosLocalRSS(channel) } else { videos = await this.getChannelVideosLocalScraper(channel) } } videoList = videoList.concat(videos) channelCount++ const percentageComplete = (channelCount / this.activeSubscriptionList.length) * 100 this.setProgressBarPercentage(percentageComplete) if (channelCount === this.activeSubscriptionList.length) { videoList = await Promise.all(videoList.sort((a, b) => { return b.publishedDate - a.publishedDate })) const profileSubscriptions = { activeProfile: this.activeProfile._id, videoList: videoList } this.videoList = await Promise.all(videoList.filter((video) => { if (this.hideWatchedSubs) { const historyIndex = this.historyCache.findIndex((x) => { return x.videoId === video.videoId }) return historyIndex === -1 } else { return true } })) this.updateProfileSubscriptions(profileSubscriptions) this.isLoading = false this.updateShowProgressBar(false) if (this.activeProfile === MAIN_PROFILE_ID) { this.updateAllSubscriptionsList(profileSubscriptions.videoList) } } }) }, getProfileSubscriptions: async function () { if (this.allSubscriptionsList.length !== 0) { this.isLoading = true this.videoList = await Promise.all(this.allSubscriptionsList.filter((video) => { const channelIndex = this.activeSubscriptionList.findIndex((x) => { return x.id === video.authorId }) const historyIndex = this.historyCache.findIndex((x) => { return x.videoId === video.videoId }) if (this.hideWatchedSubs) { return channelIndex !== -1 && historyIndex === -1 } else { return channelIndex !== -1 } })) this.isLoading = false } else { this.getSubscriptions() } }, getChannelVideosLocalScraper: function (channel, failedAttempts = 0) { return new Promise((resolve, reject) => { ytch.getChannelVideos(channel.id, 'latest').then(async (response) => { const videos = await Promise.all(response.items.map(async (video) => { if (video.liveNow) { video.publishedDate = new Date().getTime() } else { video.publishedDate = await this.calculatePublishedDate(video.publishedText) } return video })) resolve(videos) }).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) } }) switch (failedAttempts) { case 0: resolve(this.getChannelVideosLocalRSS(channel, failedAttempts + 1)) break case 1: if (this.backendFallback) { this.showToast({ message: this.$t('Falling back to Invidious API') }) resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)) } else { resolve([]) } break case 2: resolve(this.getChannelVideosLocalRSS(channel, failedAttempts + 1)) break default: resolve([]) } }) }) }, getChannelVideosLocalRSS: function (channel, failedAttempts = 0) { return new Promise((resolve, reject) => { const parser = new Parser() const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channel.id}` parser.parseURL(feedUrl).then(async (feed) => { const items = await Promise.all(feed.items.map((video) => { video.authorId = channel.id video.videoId = video.id.replace('yt:video:', '') video.type = 'video' video.lengthSeconds = '0:00' video.isRSS = true video.publishedDate = new Date(video.pubDate) if (video.publishedDate.toString() === 'Invalid Date') { video.publishedDate = new Date(video.isoDate) } video.publishedText = video.publishedDate.toLocaleString() return video })) resolve(items) }).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) } }) switch (failedAttempts) { case 0: resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1)) break case 1: if (this.backendFallback) { this.showToast({ message: this.$t('Falling back to Invidious API') }) resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)) } else { resolve([]) } break case 2: resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1)) break default: resolve([]) } }) }) }, getChannelVideosInvidiousScraper: function (channel, failedAttempts = 0) { return new Promise((resolve, reject) => { const subscriptionsPayload = { resource: 'channels/latest', id: channel.id, params: {} } this.invidiousAPICall(subscriptionsPayload).then(async (result) => { resolve(await Promise.all(result.map((video) => { video.publishedDate = new Date(video.published * 1000) return video }))) }).catch((err) => { console.log(err) const errorMessage = this.$t('Invidious API Error (Click to copy)') this.showToast({ message: `${errorMessage}: ${err.responseText}`, time: 10000, action: () => { navigator.clipboard.writeText(err) } }) switch (failedAttempts) { case 0: resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)) break case 1: if (this.backendFallback) { this.showToast({ message: this.$t('Falling back to the local API') }) resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1)) } else { resolve([]) } break case 2: resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)) break default: resolve([]) } }) }) }, getChannelVideosInvidiousRSS: function (channel, failedAttempts = 0) { return new Promise((resolve, reject) => { const parser = new Parser() const feedUrl = `${this.currentInvidiousInstance}/feed/channel/${channel.id}` parser.parseURL(feedUrl).then(async (feed) => { resolve(await Promise.all(feed.items.map((video) => { video.authorId = channel.id video.videoId = video.id.replace('yt:video:', '') video.type = 'video' video.publishedDate = new Date(video.pubDate) video.publishedText = video.publishedDate.toLocaleString() video.lengthSeconds = '0:00' video.isRSS = true return video }))) }).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) } }) switch (failedAttempts) { case 0: resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)) break case 1: if (this.backendFallback) { this.showToast({ message: this.$t('Falling back to the local API') }) resolve(this.getChannelVideosLocalRSS(channel, failedAttempts + 1)) } else { resolve([]) } break case 2: resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)) break default: resolve([]) } }) }) }, increaseLimit: function () { this.dataLimit += 100 sessionStorage.setItem('subscriptionLimit', this.dataLimit) }, ...mapActions([ 'showToast', 'invidiousAPICall', 'updateShowProgressBar', 'updateProfileSubscriptions', 'updateAllSubscriptionsList', 'calculatePublishedDate' ]), ...mapMutations([ 'setProgressBarPercentage' ]) } })