From 984b0239a054129ba71e9d4d293315e86468882e Mon Sep 17 00:00:00 2001 From: Aditya Mishra Date: Fri, 10 Apr 2026 22:54:55 +0530 Subject: [PATCH 1/5] fix(watch): show persistent error for unavailable and removed videos --- src/renderer/views/Watch/Watch.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 1f2458b9c91ef..21ceead89de3e 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -865,11 +865,16 @@ export default defineComponent({ copyToClipboard(err) }) console.error(err) - if (this.backendPreference === 'local' && this.backendFallback && !err.toString().includes('private')) { + if (this.backendPreference === 'local' && this.backendFallback && !err.toString().includes('private') && !err.toString().includes('unavailable')) { showToast(this.$t('Falling back to Invidious API')) this.getVideoInformationInvidious() } else { this.isLoading = false + + if (!this.thumbnail) { + this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg` + } + this.errorMessage = err.message || err.toString() } } }, @@ -1050,12 +1055,16 @@ export default defineComponent({ showToast(`${errorMessage}: ${err}`, 10000, () => { copyToClipboard(err) }) - console.error(err) if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) { showToast(this.$t('Falling back to Local API')) this.getVideoInformationLocal() } else { this.isLoading = false + + if (!this.thumbnail) { + this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg` + } + this.errorMessage = err.message || err.toString() } }) }, From be1f0c1f95f70e09ce602b7a07fff14cbc885369 Mon Sep 17 00:00:00 2001 From: Aditya Mishra Date: Fri, 1 May 2026 21:21:07 +0530 Subject: [PATCH 2/5] Use theme-aware unavailable video thumbnails --- src/renderer/views/Watch/Watch.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 21ceead89de3e..26a383a1be3f0 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -55,6 +55,20 @@ import { MANIFEST_TYPE_SABR } from '../../helpers/player/SabrManifestParser' const MANIFEST_TYPE_DASH = 'application/dash+xml' const MANIFEST_TYPE_HLS = 'application/x-mpegurl' +const UNAVAILABLE_VIDEO_THUMBNAILS = { + light: 'https://www.youtube.com/img/desktop/unavailable/unavailable_video.png', + dark: 'https://www.youtube.com/img/desktop/unavailable/unavailable_video_dark_theme.png' +} +const LIGHT_BASE_THEMES = new Set([ + 'light', + 'pastelPink', + 'catppuccinLatte', + 'solarizedLight', + 'gruvboxLight', + 'everforestLightHard', + 'everforestLightMedium', + 'everforestLightLow' +]) export default defineComponent({ name: 'Watch', @@ -191,6 +205,9 @@ export default defineComponent({ backendFallback: function () { return this.$store.getters.getBackendFallback }, + baseTheme: function () { + return this.$store.getters.getBaseTheme + }, currentInvidiousInstanceUrl: function () { return this.$store.getters.getCurrentInvidiousInstanceUrl }, @@ -872,7 +889,7 @@ export default defineComponent({ this.isLoading = false if (!this.thumbnail) { - this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg` + this.thumbnail = this.getUnavailableVideoThumbnail() } this.errorMessage = err.message || err.toString() } @@ -1062,7 +1079,7 @@ export default defineComponent({ this.isLoading = false if (!this.thumbnail) { - this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg` + this.thumbnail = this.getUnavailableVideoThumbnail() } this.errorMessage = err.message || err.toString() } @@ -1075,6 +1092,16 @@ export default defineComponent({ return new Date(parseInt(expireString) * 1000) }, + getUnavailableVideoThumbnail: function () { + const baseTheme = this.baseTheme || 'system' + const isLightTheme = LIGHT_BASE_THEMES.has(baseTheme) || + (baseTheme === 'system' && !window.matchMedia('(prefers-color-scheme: dark)').matches) + + return isLightTheme + ? UNAVAILABLE_VIDEO_THUMBNAILS.light + : UNAVAILABLE_VIDEO_THUMBNAILS.dark + }, + /** * @param {string?} description */ From 643adc05b6d7cf8b4b21f736fa7b082d9a1d8f8e Mon Sep 17 00:00:00 2001 From: Aditya Mishra Date: Fri, 1 May 2026 21:45:45 +0530 Subject: [PATCH 3/5] Infer unavailable thumbnail theme from background color --- src/renderer/helpers/colors.js | 25 +++++++++++++++++++++---- src/renderer/views/Watch/Watch.js | 19 +++---------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/renderer/helpers/colors.js b/src/renderer/helpers/colors.js index 22aa80a529ded..2de3eb5ff1bdd 100644 --- a/src/renderer/helpers/colors.js +++ b/src/renderer/helpers/colors.js @@ -99,10 +99,27 @@ export function getRandomColor() { } export function calculateColorLuminance(colorValue) { - const cutHex = colorValue.substring(1, 7) - const colorValueR = parseInt(cutHex.substring(0, 2), 16) - const colorValueG = parseInt(cutHex.substring(2, 4), 16) - const colorValueB = parseInt(cutHex.substring(4, 6), 16) + let colorValues + + if (colorValue.startsWith('#')) { + const cutHex = colorValue.length === 4 + ? colorValue.slice(1).split('').map(value => value + value).join('') + : colorValue.substring(1, 7) + + colorValues = [ + parseInt(cutHex.substring(0, 2), 16), + parseInt(cutHex.substring(2, 4), 16), + parseInt(cutHex.substring(4, 6), 16) + ] + } else { + colorValues = colorValue.match(/\d+(\.\d+)?/g)?.slice(0, 3).map(Number) + } + + if (!colorValues || colorValues.some(value => isNaN(value))) { + return '#FFFFFF' + } + + const [colorValueR, colorValueG, colorValueB] = colorValues const luminance = (0.299 * colorValueR + 0.587 * colorValueG + 0.114 * colorValueB) / 255 diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 26a383a1be3f0..f0e12fda1a3c5 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -13,6 +13,7 @@ import WatchVideoLiveChat from '../../components/WatchVideoLiveChat/WatchVideoLi import WatchVideoPlaylist from '../../components/WatchVideoPlaylist/WatchVideoPlaylist.vue' import WatchVideoRecommendations from '../../components/WatchVideoRecommendations/WatchVideoRecommendations.vue' import FtAgeRestricted from '../../components/FtAgeRestricted/FtAgeRestricted.vue' +import { calculateColorLuminance } from '../../helpers/colors' import { buildVTTFileLocally, copyToClipboard, @@ -59,16 +60,6 @@ const UNAVAILABLE_VIDEO_THUMBNAILS = { light: 'https://www.youtube.com/img/desktop/unavailable/unavailable_video.png', dark: 'https://www.youtube.com/img/desktop/unavailable/unavailable_video_dark_theme.png' } -const LIGHT_BASE_THEMES = new Set([ - 'light', - 'pastelPink', - 'catppuccinLatte', - 'solarizedLight', - 'gruvboxLight', - 'everforestLightHard', - 'everforestLightMedium', - 'everforestLightLow' -]) export default defineComponent({ name: 'Watch', @@ -205,9 +196,6 @@ export default defineComponent({ backendFallback: function () { return this.$store.getters.getBackendFallback }, - baseTheme: function () { - return this.$store.getters.getBaseTheme - }, currentInvidiousInstanceUrl: function () { return this.$store.getters.getCurrentInvidiousInstanceUrl }, @@ -1093,9 +1081,8 @@ export default defineComponent({ }, getUnavailableVideoThumbnail: function () { - const baseTheme = this.baseTheme || 'system' - const isLightTheme = LIGHT_BASE_THEMES.has(baseTheme) || - (baseTheme === 'system' && !window.matchMedia('(prefers-color-scheme: dark)').matches) + const backgroundColor = window.getComputedStyle(document.body).backgroundColor + const isLightTheme = calculateColorLuminance(backgroundColor) === '#000000' return isLightTheme ? UNAVAILABLE_VIDEO_THUMBNAILS.light From 450b4c790c20763b16bd74b09dd1e2ce881a3b1a Mon Sep 17 00:00:00 2001 From: Aditya Mishra Date: Sat, 2 May 2026 12:39:34 +0530 Subject: [PATCH 4/5] Remove redundant toast when error is shown in player overlay --- src/renderer/views/Watch/Watch.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index f0e12fda1a3c5..7acafb8126792 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -865,12 +865,12 @@ export default defineComponent({ this.isLoading = false this.updateTitle() } catch (err) { - const errorMessage = this.$t('Local API Error (Click to copy)') - showToast(`${errorMessage}: ${err}`, 10000, () => { - copyToClipboard(err) - }) console.error(err) if (this.backendPreference === 'local' && this.backendFallback && !err.toString().includes('private') && !err.toString().includes('unavailable')) { + const errorMessage = this.$t('Local API Error (Click to copy)') + showToast(`${errorMessage}: ${err}`, 10000, () => { + copyToClipboard(err) + }) showToast(this.$t('Falling back to Invidious API')) this.getVideoInformationInvidious() } else { @@ -1056,11 +1056,11 @@ export default defineComponent({ }) .catch(err => { console.error(err) - const errorMessage = this.$t('Invidious API Error (Click to copy)') - showToast(`${errorMessage}: ${err}`, 10000, () => { - copyToClipboard(err) - }) if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) { + const errorMessage = this.$t('Invidious API Error (Click to copy)') + showToast(`${errorMessage}: ${err}`, 10000, () => { + copyToClipboard(err) + }) showToast(this.$t('Falling back to Local API')) this.getVideoInformationLocal() } else { From 8a5b197b15b4ea9de61f9453f2a6fc731f347082 Mon Sep 17 00:00:00 2001 From: Aditya Mishra Date: Sun, 3 May 2026 10:19:53 +0530 Subject: [PATCH 5/5] fix(watch): clear stale video data on route change --- src/renderer/views/Watch/Watch.js | 56 ++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 7acafb8126792..0c4a0f51fa30e 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -350,18 +350,11 @@ export default defineComponent({ // react to route changes... this.videoId = this.$route.params.id + this.resetVideoState() this.firstLoad = true this.videoPlayerLoaded = false - this.errorMessage = null - this.customErrorIcon = null this.activeFormat = this.defaultVideoFormat - this.sabrData = null - this.videoStoryboardSrc = '' - this.captions = [] - this.vrProjection = null - this.videoCurrentChapterIndex = 0 - this.videoGenreIsMusic = false this.checkIfTimestamp() this.checkIfPlaylist() @@ -375,6 +368,53 @@ export default defineComponent({ break } }, + + resetVideoState: function () { + this.isLoading = true + this.isFamilyFriendly = false + this.isLive = false + this.liveChat = null + this.isLiveContent = false + this.isUpcoming = false + this.isPostLiveDvr = false + this.isUnlisted = false + this.upcomingTimestamp = null + this.upcomingTimeLeft = null + this.thumbnail = '' + this.videoTitle = '' + this.videoDescription = '' + this.videoDescriptionHtml = '' + this.license = '' + this.videoViewCount = 0 + this.videoLikeCount = 0 + this.videoDislikeCount = 0 + this.videoLengthSeconds = 0 + this.videoChapters = [] + this.videoCurrentChapterIndex = 0 + this.videoChaptersKind = 'chapters' + this.channelName = '' + this.channelThumbnail = '' + this.channelId = '' + this.channelSubscriptionCountText = '' + this.videoPublished = 0 + this.premiereDate = undefined + this.videoStoryboardSrc = '' + this.manifestSrc = null + this.manifestMimeType = MANIFEST_TYPE_DASH + this.sabrData = null + this.legacyFormats = [] + this.captions = [] + this.vrProjection = null + this.recommendedVideos = [] + this.playabilityStatus = '' + this.adEndTimeUnixMs = 0 + this.errorMessage = null + this.customErrorIcon = null + this.videoGenreIsMusic = false + this.streamingDataExpiryDate = null + this.updateTitle() + }, + onMountedDependOnLocalStateLoading() { // Prevent running twice if (this.onMountedRun) { return }