From fbef37daaff25bd6e4c442b082b186f57d4a3457 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:14:20 +0100 Subject: [PATCH] Handle unplayable content better with the local API (#5922) --- src/renderer/helpers/api/local.js | 62 +++++++++------------- src/renderer/main.js | 2 + src/renderer/views/Watch/Watch.js | 85 +++++++++++++++++++++---------- static/locales/en-US.yaml | 2 + 4 files changed, 86 insertions(+), 65 deletions(-) diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index cd9f1da96c5b3..ff43c895ec9b0 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -253,49 +253,35 @@ export async function getLocalVideoInfo(id) { id = trailerScreen.video_id } - // try to bypass the age restriction - if (info.playability_status.status === 'LOGIN_REQUIRED' || (hasTrailer && trailerIsAgeRestricted)) { - const tvInnertube = await createInnertube({ withPlayer: true, clientType: ClientType.TV_EMBEDDED, generateSessionLocally: false }) - - const tvInfo = await tvInnertube.getBasicInfo(id, 'TV_EMBEDDED') - - if (tvInfo.streaming_data) { - decipherFormats(tvInfo.streaming_data.adaptive_formats, tvInnertube.actions.session.player) - decipherFormats(tvInfo.streaming_data.formats, tvInnertube.actions.session.player) - } - - info.playability_status = tvInfo.playability_status - info.streaming_data = tvInfo.streaming_data - info.basic_info.start_timestamp = tvInfo.basic_info.start_timestamp - info.basic_info.duration = tvInfo.basic_info.duration - info.captions = tvInfo.captions - info.storyboards = tvInfo.storyboards - } else { - const iosInnertube = await createInnertube({ clientType: ClientType.IOS }) - - const iosInfo = await iosInnertube.getBasicInfo(id, 'iOS') + if ((info.playability_status.status === 'UNPLAYABLE' && (!hasTrailer || trailerIsAgeRestricted)) || + info.playability_status.status === 'LOGIN_REQUIRED') { + return info + } - if (hasTrailer) { - info.playability_status = iosInfo.playability_status - info.streaming_data = iosInfo.streaming_data - info.basic_info.start_timestamp = iosInfo.basic_info.start_timestamp - info.basic_info.duration = iosInfo.basic_info.duration - info.captions = iosInfo.captions - info.storyboards = iosInfo.storyboards - } else if (iosInfo.streaming_data) { - info.streaming_data.adaptive_formats = iosInfo.streaming_data.adaptive_formats - info.streaming_data.hls_manifest_url = iosInfo.streaming_data.hls_manifest_url + const iosInnertube = await createInnertube({ clientType: ClientType.IOS }) - // Use the legacy formats from the original web response as the iOS client doesn't have any legacy formats + const iosInfo = await iosInnertube.getBasicInfo(id, 'iOS') - for (const format of info.streaming_data.adaptive_formats) { - format.freeTubeUrl = format.url - } + if (hasTrailer) { + info.playability_status = iosInfo.playability_status + info.streaming_data = iosInfo.streaming_data + info.basic_info.start_timestamp = iosInfo.basic_info.start_timestamp + info.basic_info.duration = iosInfo.basic_info.duration + info.captions = iosInfo.captions + info.storyboards = iosInfo.storyboards + } else if (iosInfo.streaming_data) { + info.streaming_data.adaptive_formats = iosInfo.streaming_data.adaptive_formats + info.streaming_data.hls_manifest_url = iosInfo.streaming_data.hls_manifest_url + + // Use the legacy formats from the original web response as the iOS client doesn't have any legacy formats + + for (const format of info.streaming_data.adaptive_formats) { + format.freeTubeUrl = format.url } + } - if (info.streaming_data) { - decipherFormats(info.streaming_data.formats, webInnertube.actions.session.player) - } + if (info.streaming_data) { + decipherFormats(info.streaming_data.formats, webInnertube.actions.session.player) } return info diff --git a/src/renderer/main.js b/src/renderer/main.js index 0725d4b0a4b29..548eb98caeaa7 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -70,6 +70,7 @@ import { faList, faLocationDot, faLock, + faMoneyCheckDollar, faNetworkWired, faNewspaper, faPalette, @@ -182,6 +183,7 @@ library.add( faList, faLocationDot, faLock, + faMoneyCheckDollar, faNetworkWired, faNewspaper, faPalette, diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 63b8d80704172..d334ca46989cf 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -15,6 +15,7 @@ import packageDetails from '../../../../package.json' import { buildVTTFileLocally, copyToClipboard, + extractNumberFromString, formatDurationAsTimestamp, formatNumber, showToast @@ -356,31 +357,12 @@ export default defineComponent({ return } - const playabilityStatus = result.playability_status - - // The apostrophe is intentionally that one (char code 8217), because that is the one YouTube uses - const BOT_MESSAGE = 'Sign in to confirm you’re not a bot' - - if (playabilityStatus.status === 'UNPLAYABLE' || (playabilityStatus.status === 'LOGIN_REQUIRED' && playabilityStatus.reason === BOT_MESSAGE)) { - if (playabilityStatus.reason === BOT_MESSAGE) { - throw new Error(this.$t('Video.IP block')) - } - - let errorText = `[${playabilityStatus.status}] ${playabilityStatus.reason}` - - if (playabilityStatus.error_screen) { - errorText += `: ${playabilityStatus.error_screen.subreason.text}` - } - - throw new Error(errorText) - } - // extract localised title first and fall back to the not localised one this.videoTitle = result.primary_info?.title.text ?? result.basic_info.title - this.videoViewCount = result.basic_info.view_count + this.videoViewCount = result.basic_info.view_count ?? extractNumberFromString(result.primary_info.view_count.text) - this.channelId = result.basic_info.channel_id - this.channelName = result.basic_info.author + this.channelId = result.basic_info.channel_id ?? result.secondary_info.owner?.author.id + this.channelName = result.basic_info.author ?? result.secondary_info.owner?.author.name if (result.secondary_info.owner?.author) { this.channelThumbnail = result.secondary_info.owner.author.best_thumbnail?.url ?? '' @@ -396,8 +378,13 @@ export default defineComponent({ channelId: this.channelId }) - // `result.page[0].microformat.publish_date` example value: `2023-08-12T08:59:59-07:00` - this.videoPublished = new Date(result.page[0].microformat.publish_date).getTime() + if (result.page[0].microformat?.publish_date) { + // `result.page[0].microformat.publish_date` example value: `2023-08-12T08:59:59-07:00` + this.videoPublished = new Date(result.page[0].microformat.publish_date).getTime() + } else { + // text date Jan 1, 2000, not as accurate but better than nothing + this.videoPublished = new Date(result.primary_info.published).getTime() + } if (result.secondary_info?.description.runs) { try { @@ -421,7 +408,7 @@ export default defineComponent({ this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxres3.jpg` break default: - this.thumbnail = result.basic_info.thumbnail[0].url + this.thumbnail = result.basic_info.thumbnail?.[0].url ?? `https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg` break } @@ -465,7 +452,7 @@ export default defineComponent({ }) } } else { - chapters = this.extractChaptersFromDescription(result.basic_info.short_description) + chapters = this.extractChaptersFromDescription(result.basic_info.short_description ?? result.secondary_info.description.text) } if (chapters.length > 0) { @@ -481,6 +468,51 @@ export default defineComponent({ this.videoChapters = chapters + const playabilityStatus = result.playability_status + + // The apostrophe is intentionally that one (char code 8217), because that is the one YouTube uses + const BOT_MESSAGE = 'Sign in to confirm you’re not a bot' + + if (playabilityStatus.status === 'UNPLAYABLE' || playabilityStatus.status === 'LOGIN_REQUIRED') { + if (playabilityStatus.error_screen?.offer_id === 'sponsors_only_video') { + // Members-only videos can only be watched while logged into a Google account that is a paid channel member + // so there is no point trying any other backends as it will always fail + this.errorMessage = this.$t('Video.MembersOnly') + this.customErrorIcon = ['fas', 'money-check-dollar'] + this.isLoading = false + this.updateTitle() + return + } else if (playabilityStatus.reason === 'Sign in to confirm your age' || (result.has_trailer && result.getTrailerInfo() === null)) { + // Age-restricted videos can only be watched while logged into a Google account that is age-verified + // so there is no point trying any other backends as it will always fail + this.errorMessage = this.$t('Video.AgeRestricted') + this.isLoading = false + this.updateTitle() + return + } + + let errorText + + if (playabilityStatus.reason === BOT_MESSAGE || playabilityStatus.reason === 'Please sign in') { + errorText = this.$t('Video.IP block') + } else { + errorText = `[${playabilityStatus.status}] ${playabilityStatus.reason}` + + if (playabilityStatus.error_screen?.subreason) { + errorText += `: ${playabilityStatus.error_screen.subreason.text}` + } + } + + if (this.backendFallback) { + throw new Error(errorText) + } else { + this.errorMessage = errorText + this.isLoading = false + this.updateTitle() + return + } + } + if (!this.hideLiveChat && this.isLive && result.livechat) { this.liveChat = result.getLiveChat() } else { @@ -700,7 +732,6 @@ export default defineComponent({ } } - // this.errorMessage = 'Test error message' this.isLoading = false this.updateTitle() } catch (err) { diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index 5b0b38bdb8e8a..20634be0c2f18 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -765,6 +765,8 @@ Channel: Viewing Posts Only Supported By Invidious: Viewing Posts is only supported by Invidious. Head to a channel's community tab to view content there without Invidious. Video: IP block: 'YouTube has blocked your IP address from watching videos. Please try switching to a different VPN or proxy.' + MembersOnly: Members-only videos cannot be watched with FreeTube as they require Google login and paid membership to the uploader's channel. + AgeRestricted: Age-restricted videos cannot be watched with FreeTube as they require Google login and using an age-verified YouTube account. More Options: More Options Mark As Watched: Mark As Watched Remove From History: Remove From History