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 1f2458b9c91ef..0c4a0f51fa30e 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, @@ -55,6 +56,10 @@ 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' +} export default defineComponent({ name: 'Watch', @@ -345,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() @@ -370,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 } @@ -860,16 +905,21 @@ 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')) { + 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 { this.isLoading = false + + if (!this.thumbnail) { + this.thumbnail = this.getUnavailableVideoThumbnail() + } + this.errorMessage = err.message || err.toString() } } }, @@ -1045,17 +1095,21 @@ export default defineComponent({ this.isLoading = false }) .catch(err => { - console.error(err) - const errorMessage = this.$t('Invidious API Error (Click to copy)') - showToast(`${errorMessage}: ${err}`, 10000, () => { - copyToClipboard(err) - }) console.error(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 { this.isLoading = false + + if (!this.thumbnail) { + this.thumbnail = this.getUnavailableVideoThumbnail() + } + this.errorMessage = err.message || err.toString() } }) }, @@ -1066,6 +1120,15 @@ export default defineComponent({ return new Date(parseInt(expireString) * 1000) }, + getUnavailableVideoThumbnail: function () { + const backgroundColor = window.getComputedStyle(document.body).backgroundColor + const isLightTheme = calculateColorLuminance(backgroundColor) === '#000000' + + return isLightTheme + ? UNAVAILABLE_VIDEO_THUMBNAILS.light + : UNAVAILABLE_VIDEO_THUMBNAILS.dark + }, + /** * @param {string?} description */