From 393f889d9bae8a78b35d6970b1f125ddbe5d1705 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:49:54 +0100 Subject: [PATCH] Update channel names and thumbnails when refreshing subscriptions (#4688) --- .../subscriptions-community.js | 31 +++++++ .../subscriptions-live/subscriptions-live.js | 87 ++++++++++++++----- .../subscriptions-shorts.js | 43 +++++++-- .../subscriptions-videos.js | 87 ++++++++++++++----- src/renderer/helpers/api/local.js | 36 ++++++-- src/renderer/helpers/subscriptions.js | 9 +- src/renderer/store/modules/profiles.js | 37 ++++++++ 7 files changed, 272 insertions(+), 58 deletions(-) diff --git a/src/renderer/components/subscriptions-community/subscriptions-community.js b/src/renderer/components/subscriptions-community/subscriptions-community.js index 38c804c86..d3042f919 100644 --- a/src/renderer/components/subscriptions-community/subscriptions-community.js +++ b/src/renderer/components/subscriptions-community/subscriptions-community.js @@ -121,6 +121,8 @@ export default defineComponent({ this.attemptedFetch = true this.errorChannels = [] + const subscriptionUpdates = [] + const postListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => { let posts = [] if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') { @@ -137,6 +139,32 @@ export default defineComponent({ channelId: channel.id, posts: posts, }) + + if (posts.length > 0) { + const post = posts.find(post => post.authorId === channel.id) + + if (post) { + const name = post.author + let thumbnailUrl = post.authorThumbnails?.[0]?.url + + if (name || thumbnailUrl) { + if (thumbnailUrl) { + if (thumbnailUrl.startsWith('//')) { + thumbnailUrl = 'https:' + thumbnailUrl + } else if (thumbnailUrl.startsWith(`${this.currentInvidiousInstance}/ggpht`)) { + thumbnailUrl = thumbnailUrl.replace(`${this.currentInvidiousInstance}/ggpht`, 'https://yt3.googleusercontent.com') + } + } + + subscriptionUpdates.push({ + channelId: channel.id, + channelName: name, + channelThumbnailUrl: thumbnailUrl + }) + } + } + } + return posts }))).flatMap((o) => o) postList.push(...postListFromRemote) @@ -147,6 +175,8 @@ export default defineComponent({ this.postList = postList this.isLoading = false this.updateShowProgressBar(false) + + this.batchUpdateSubscriptionDetails(subscriptionUpdates) }, maybeLoadPostsForSubscriptionsFromRemote: async function () { @@ -211,6 +241,7 @@ export default defineComponent({ ...mapActions([ 'updateShowProgressBar', + 'batchUpdateSubscriptionDetails', 'updateSubscriptionPostsCacheByChannel', ]), diff --git a/src/renderer/components/subscriptions-live/subscriptions-live.js b/src/renderer/components/subscriptions-live/subscriptions-live.js index ca227b271..0e3a7b313 100644 --- a/src/renderer/components/subscriptions-live/subscriptions-live.js +++ b/src/renderer/components/subscriptions-live/subscriptions-live.js @@ -129,19 +129,23 @@ export default defineComponent({ this.attemptedFetch = true this.errorChannels = [] + const subscriptionUpdates = [] + const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => { let videos = [] + let name, thumbnailUrl + if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') { if (useRss) { - videos = await this.getChannelLiveInvidiousRSS(channel) + ({ videos, name, thumbnailUrl } = await this.getChannelLiveInvidiousRSS(channel)) } else { - videos = await this.getChannelLiveInvidious(channel) + ({ videos, name, thumbnailUrl } = await this.getChannelLiveInvidious(channel)) } } else { if (useRss) { - videos = await this.getChannelLiveLocalRSS(channel) + ({ videos, name, thumbnailUrl } = await this.getChannelLiveLocalRSS(channel)) } else { - videos = await this.getChannelLiveLocal(channel) + ({ videos, name, thumbnailUrl } = await this.getChannelLiveLocal(channel)) } } @@ -152,6 +156,15 @@ export default defineComponent({ 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) @@ -159,6 +172,8 @@ export default defineComponent({ this.videoList = updateVideoListAfterProcessing(videoList) this.isLoading = false this.updateShowProgressBar(false) + + this.batchUpdateSubscriptionDetails(subscriptionUpdates) }, maybeLoadVideosForSubscriptionsFromRemote: async function () { @@ -174,16 +189,18 @@ export default defineComponent({ getChannelLiveLocal: async function (channel, failedAttempts = 0) { try { - const entries = await getLocalChannelLiveStreams(channel.id) + const result = await getLocalChannelLiveStreams(channel.id) - if (entries === null) { + if (result === null) { this.errorChannels.push(channel) - return [] + return { + videos: [] + } } - addPublishedDatesLocal(entries) + addPublishedDatesLocal(result.videos) - return entries + return result } catch (err) { console.error(err) const errorMessage = this.$t('Local API Error (Click to copy)') @@ -198,12 +215,16 @@ export default defineComponent({ showToast(this.$t('Falling back to Invidious API')) return await this.getChannelLiveInvidious(channel, failedAttempts + 1) } else { - return [] + return { + videos: [] + } } case 2: return await this.getChannelLiveLocalRSS(channel, failedAttempts + 1) default: - return [] + return { + videos: [] + } } } }, @@ -227,7 +248,9 @@ export default defineComponent({ this.errorChannels.push(channel) } - return [] + return { + videos: [] + } } return await parseYouTubeRSSFeed(await response.text(), channel.id) @@ -245,12 +268,16 @@ export default defineComponent({ showToast(this.$t('Falling back to Invidious API')) return this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1) } else { - return [] + return { + videos: [] + } } case 2: return this.getChannelLiveLocal(channel, failedAttempts + 1) default: - return [] + return { + videos: [] + } } } }, @@ -269,7 +296,16 @@ export default defineComponent({ addPublishedDatesInvidious(videos) - resolve(videos) + let name + + if (videos.length > 0) { + name = videos.find(video => video.author).author + } + + resolve({ + name, + videos + }) }).catch((err) => { console.error(err) const errorMessage = this.$t('Invidious API Error (Click to copy)') @@ -285,14 +321,18 @@ export default defineComponent({ showToast(this.$t('Falling back to Local API')) resolve(this.getChannelLiveLocal(channel, failedAttempts + 1)) } else { - resolve([]) + resolve({ + videos: [] + }) } break case 2: resolve(this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1)) break default: - resolve([]) + resolve({ + videos: [] + }) } }) }) @@ -306,7 +346,9 @@ export default defineComponent({ const response = await fetch(feedUrl) if (response.status === 500 || response.status === 404) { - return [] + return { + videos: [] + } } return await parseYouTubeRSSFeed(await response.text(), channel.id) @@ -324,17 +366,22 @@ export default defineComponent({ showToast(this.$t('Falling back to Local API')) return this.getChannelLiveLocalRSS(channel, failedAttempts + 1) } else { - return [] + return { + videos: [] + } } case 2: return this.getChannelLiveInvidious(channel, failedAttempts + 1) default: - return [] + return { + videos: [] + } } } }, ...mapActions([ + 'batchUpdateSubscriptionDetails', 'updateShowProgressBar', 'updateSubscriptionLiveCacheByChannel', ]), diff --git a/src/renderer/components/subscriptions-shorts/subscriptions-shorts.js b/src/renderer/components/subscriptions-shorts/subscriptions-shorts.js index def9312e2..9a3c45c3c 100644 --- a/src/renderer/components/subscriptions-shorts/subscriptions-shorts.js +++ b/src/renderer/components/subscriptions-shorts/subscriptions-shorts.js @@ -114,12 +114,16 @@ export default defineComponent({ this.attemptedFetch = true this.errorChannels = [] + const subscriptionUpdates = [] + const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => { let videos = [] + let name + if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') { - videos = await this.getChannelShortsInvidious(channel) + ({ videos, name } = await this.getChannelShortsInvidious(channel)) } else { - videos = await this.getChannelShortsLocal(channel) + ({ videos, name } = await this.getChannelShortsLocal(channel)) } channelCount++ @@ -129,6 +133,14 @@ export default defineComponent({ channelId: channel.id, videos: videos, }) + + if (name) { + subscriptionUpdates.push({ + channelId: channel.id, + channelName: name + }) + } + return videos }))).flatMap((o) => o) videoList.push(...videoListFromRemote) @@ -136,6 +148,8 @@ export default defineComponent({ this.videoList = updateVideoListAfterProcessing(videoList) this.isLoading = false this.updateShowProgressBar(false) + + this.batchUpdateSubscriptionDetails(subscriptionUpdates) }, maybeLoadVideosForSubscriptionsFromRemote: async function () { @@ -168,7 +182,9 @@ export default defineComponent({ this.errorChannels.push(channel) } - return [] + return { + videos: [] + } } return await parseYouTubeRSSFeed(await response.text(), channel.id) @@ -184,10 +200,14 @@ export default defineComponent({ showToast(this.$t('Falling back to Invidious API')) return this.getChannelShortsInvidious(channel, failedAttempts + 1) } else { - return [] + return { + videos: [] + } } default: - return [] + return { + videos: [] + } } } }, @@ -200,7 +220,9 @@ export default defineComponent({ const response = await fetch(feedUrl) if (response.status === 500 || response.status === 404) { - return [] + return { + videos: [] + } } return await parseYouTubeRSSFeed(await response.text(), channel.id) @@ -216,15 +238,20 @@ export default defineComponent({ showToast(this.$t('Falling back to Local API')) return this.getChannelShortsLocal(channel, failedAttempts + 1) } else { - return [] + return { + videos: [] + } } default: - return [] + return { + videos: [] + } } } }, ...mapActions([ + 'batchUpdateSubscriptionDetails', 'updateShowProgressBar', 'updateSubscriptionShortsCacheByChannel', ]), diff --git a/src/renderer/components/subscriptions-videos/subscriptions-videos.js b/src/renderer/components/subscriptions-videos/subscriptions-videos.js index d82e0fb79..aea208a2d 100644 --- a/src/renderer/components/subscriptions-videos/subscriptions-videos.js +++ b/src/renderer/components/subscriptions-videos/subscriptions-videos.js @@ -129,19 +129,23 @@ export default defineComponent({ this.attemptedFetch = true this.errorChannels = [] + const subscriptionUpdates = [] + const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => { let videos = [] + let name, thumbnailUrl + if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') { if (useRss) { - videos = await this.getChannelVideosInvidiousRSS(channel) + ({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousRSS(channel)) } else { - videos = await this.getChannelVideosInvidiousScraper(channel) + ({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousScraper(channel)) } } else { if (useRss) { - videos = await this.getChannelVideosLocalRSS(channel) + ({ videos, name, thumbnailUrl } = await this.getChannelVideosLocalRSS(channel)) } else { - videos = await this.getChannelVideosLocalScraper(channel) + ({ videos, name, thumbnailUrl } = await this.getChannelVideosLocalScraper(channel)) } } @@ -152,6 +156,15 @@ export default defineComponent({ 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) @@ -159,6 +172,8 @@ export default defineComponent({ this.videoList = updateVideoListAfterProcessing(videoList) this.isLoading = false this.updateShowProgressBar(false) + + this.batchUpdateSubscriptionDetails(subscriptionUpdates) }, maybeLoadVideosForSubscriptionsFromRemote: async function () { @@ -174,16 +189,18 @@ export default defineComponent({ getChannelVideosLocalScraper: async function (channel, failedAttempts = 0) { try { - const videos = await getLocalChannelVideos(channel.id) + const result = await getLocalChannelVideos(channel.id) - if (videos === null) { + if (result === null) { this.errorChannels.push(channel) - return [] + return { + videos: [] + } } - addPublishedDatesLocal(videos) + addPublishedDatesLocal(result.videos) - return videos + return result } catch (err) { console.error(err) const errorMessage = this.$t('Local API Error (Click to copy)') @@ -198,12 +215,16 @@ export default defineComponent({ showToast(this.$t('Falling back to Invidious API')) return await this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1) } else { - return [] + return { + videos: [] + } } case 2: return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1) default: - return [] + return { + videos: [] + } } } }, @@ -227,7 +248,9 @@ export default defineComponent({ this.errorChannels.push(channel) } - return [] + return { + videos: [] + } } return await parseYouTubeRSSFeed(await response.text(), channel.id) @@ -245,12 +268,16 @@ export default defineComponent({ showToast(this.$t('Falling back to Invidious API')) return this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1) } else { - return [] + return { + videos: [] + } } case 2: return this.getChannelVideosLocalScraper(channel, failedAttempts + 1) default: - return [] + return { + videos: [] + } } } }, @@ -266,7 +293,16 @@ export default defineComponent({ invidiousAPICall(subscriptionsPayload).then((result) => { addPublishedDatesInvidious(result.videos) - resolve(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)') @@ -282,14 +318,18 @@ export default defineComponent({ showToast(this.$t('Falling back to Local API')) resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1)) } else { - resolve([]) + resolve({ + videos: [] + }) } break case 2: resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)) break default: - resolve([]) + resolve({ + videos: [] + }) } }) }) @@ -304,7 +344,9 @@ export default defineComponent({ if (response.status === 500 || response.status === 404) { this.errorChannels.push(channel) - return [] + return { + videos: [] + } } return await parseYouTubeRSSFeed(await response.text(), channel.id) @@ -322,17 +364,22 @@ export default defineComponent({ showToast(this.$t('Falling back to Local API')) return this.getChannelVideosLocalRSS(channel, failedAttempts + 1) } else { - return [] + return { + videos: [] + } } case 2: return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1) default: - return [] + return { + videos: [] + } } } }, ...mapActions([ + 'batchUpdateSubscriptionDetails', 'updateShowProgressBar', 'updateSubscriptionVideosCacheByChannel', ]), diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index a74434957..0f3b08cda 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -274,6 +274,9 @@ export async function getLocalChannel(id) { return result } +/** + * @param {string} id + */ export async function getLocalChannelVideos(id) { const innertube = await createInnertube() @@ -286,15 +289,22 @@ export async function getLocalChannelVideos(id) { })) const videosTab = new YT.Channel(null, response) + const { id: channelId = id, name, thumbnailUrl } = parseLocalChannelHeader(videosTab) + + let videos // if the channel doesn't have a videos tab, YouTube returns the home tab instead // so we need to check that we got the right tab if (videosTab.current_tab?.endpoint.metadata.url?.endsWith('/videos')) { - const { id: channelId = id, name } = parseLocalChannelHeader(videosTab) - - return parseLocalChannelVideos(videosTab.videos, channelId, name) + videos = parseLocalChannelVideos(videosTab.videos, channelId, name) } else { - return [] + videos = [] + } + + return { + name, + thumbnailUrl, + videos } } catch (error) { console.error(error) @@ -306,6 +316,9 @@ export async function getLocalChannelVideos(id) { } } +/** + * @param {string} id + */ export async function getLocalChannelLiveStreams(id) { const innertube = await createInnertube() @@ -318,15 +331,22 @@ export async function getLocalChannelLiveStreams(id) { })) const liveStreamsTab = new YT.Channel(null, response) + const { id: channelId = id, name, thumbnailUrl } = parseLocalChannelHeader(liveStreamsTab) + + let videos // if the channel doesn't have a live tab, YouTube returns the home tab instead // so we need to check that we got the right tab if (liveStreamsTab.current_tab?.endpoint.metadata.url?.endsWith('/streams')) { - const { id: channelId = id, name } = parseLocalChannelHeader(liveStreamsTab) - - return parseLocalChannelVideos(liveStreamsTab.videos, channelId, name) + videos = parseLocalChannelVideos(liveStreamsTab.videos, channelId, name) } else { - return [] + videos = [] + } + + return { + name, + thumbnailUrl, + videos } } catch (error) { console.error(error) diff --git a/src/renderer/helpers/subscriptions.js b/src/renderer/helpers/subscriptions.js index c99b0b843..41bef51bd 100644 --- a/src/renderer/helpers/subscriptions.js +++ b/src/renderer/helpers/subscriptions.js @@ -80,9 +80,14 @@ export async function parseYouTubeRSSFeed(rssString, channelId) { promises.push(parseRSSEntry(entry, channelId, channelName)) } - return await Promise.all(promises) + return { + name: channelName, + videos: await Promise.all(promises) + } } catch (e) { - return [] + return { + videos: [] + } } } diff --git a/src/renderer/store/modules/profiles.js b/src/renderer/store/modules/profiles.js index 3ee50d7d4..02630feff 100644 --- a/src/renderer/store/modules/profiles.js +++ b/src/renderer/store/modules/profiles.js @@ -91,6 +91,43 @@ const actions = { commit('setProfileList', profiles) }, + async batchUpdateSubscriptionDetails({ getters, dispatch }, channels) { + if (channels.length === 0) { return } + + const profileList = getters.getProfileList + + for (const profile of profileList) { + const currentProfileCopy = deepCopy(profile) + let profileUpdated = false + + for (const { channelThumbnailUrl, channelName, channelId } of channels) { + const channel = currentProfileCopy.subscriptions.find((channel) => { + return channel.id === channelId + }) ?? null + + if (channel === null) { continue } + + if (channel.name !== channelName && channelName != null) { + channel.name = channelName + profileUpdated = true + } + + if (channelThumbnailUrl) { + const thumbnail = channelThumbnailUrl.replace(/=s\d*/, '=s176') // change thumbnail size if different + + if (channel.thumbnail !== thumbnail) { + channel.thumbnail = thumbnail + profileUpdated = true + } + } + } + + if (profileUpdated) { + await dispatch('updateProfile', currentProfileCopy) + } + } + }, + async updateSubscriptionDetails({ getters, dispatch }, { channelThumbnailUrl, channelName, channelId }) { const thumbnail = channelThumbnailUrl?.replace(/=s\d*/, '=s176') ?? null // change thumbnail size if different const profileList = getters.getProfileList