Skip to content

Commit fbef37d

Browse files
authored
Handle unplayable content better with the local API (#5922)
1 parent 999e48b commit fbef37d

File tree

4 files changed

+86
-65
lines changed

4 files changed

+86
-65
lines changed

src/renderer/helpers/api/local.js

+24-38
Original file line numberDiff line numberDiff line change
@@ -253,49 +253,35 @@ export async function getLocalVideoInfo(id) {
253253
id = trailerScreen.video_id
254254
}
255255

256-
// try to bypass the age restriction
257-
if (info.playability_status.status === 'LOGIN_REQUIRED' || (hasTrailer && trailerIsAgeRestricted)) {
258-
const tvInnertube = await createInnertube({ withPlayer: true, clientType: ClientType.TV_EMBEDDED, generateSessionLocally: false })
259-
260-
const tvInfo = await tvInnertube.getBasicInfo(id, 'TV_EMBEDDED')
261-
262-
if (tvInfo.streaming_data) {
263-
decipherFormats(tvInfo.streaming_data.adaptive_formats, tvInnertube.actions.session.player)
264-
decipherFormats(tvInfo.streaming_data.formats, tvInnertube.actions.session.player)
265-
}
266-
267-
info.playability_status = tvInfo.playability_status
268-
info.streaming_data = tvInfo.streaming_data
269-
info.basic_info.start_timestamp = tvInfo.basic_info.start_timestamp
270-
info.basic_info.duration = tvInfo.basic_info.duration
271-
info.captions = tvInfo.captions
272-
info.storyboards = tvInfo.storyboards
273-
} else {
274-
const iosInnertube = await createInnertube({ clientType: ClientType.IOS })
275-
276-
const iosInfo = await iosInnertube.getBasicInfo(id, 'iOS')
256+
if ((info.playability_status.status === 'UNPLAYABLE' && (!hasTrailer || trailerIsAgeRestricted)) ||
257+
info.playability_status.status === 'LOGIN_REQUIRED') {
258+
return info
259+
}
277260

278-
if (hasTrailer) {
279-
info.playability_status = iosInfo.playability_status
280-
info.streaming_data = iosInfo.streaming_data
281-
info.basic_info.start_timestamp = iosInfo.basic_info.start_timestamp
282-
info.basic_info.duration = iosInfo.basic_info.duration
283-
info.captions = iosInfo.captions
284-
info.storyboards = iosInfo.storyboards
285-
} else if (iosInfo.streaming_data) {
286-
info.streaming_data.adaptive_formats = iosInfo.streaming_data.adaptive_formats
287-
info.streaming_data.hls_manifest_url = iosInfo.streaming_data.hls_manifest_url
261+
const iosInnertube = await createInnertube({ clientType: ClientType.IOS })
288262

289-
// Use the legacy formats from the original web response as the iOS client doesn't have any legacy formats
263+
const iosInfo = await iosInnertube.getBasicInfo(id, 'iOS')
290264

291-
for (const format of info.streaming_data.adaptive_formats) {
292-
format.freeTubeUrl = format.url
293-
}
265+
if (hasTrailer) {
266+
info.playability_status = iosInfo.playability_status
267+
info.streaming_data = iosInfo.streaming_data
268+
info.basic_info.start_timestamp = iosInfo.basic_info.start_timestamp
269+
info.basic_info.duration = iosInfo.basic_info.duration
270+
info.captions = iosInfo.captions
271+
info.storyboards = iosInfo.storyboards
272+
} else if (iosInfo.streaming_data) {
273+
info.streaming_data.adaptive_formats = iosInfo.streaming_data.adaptive_formats
274+
info.streaming_data.hls_manifest_url = iosInfo.streaming_data.hls_manifest_url
275+
276+
// Use the legacy formats from the original web response as the iOS client doesn't have any legacy formats
277+
278+
for (const format of info.streaming_data.adaptive_formats) {
279+
format.freeTubeUrl = format.url
294280
}
281+
}
295282

296-
if (info.streaming_data) {
297-
decipherFormats(info.streaming_data.formats, webInnertube.actions.session.player)
298-
}
283+
if (info.streaming_data) {
284+
decipherFormats(info.streaming_data.formats, webInnertube.actions.session.player)
299285
}
300286

301287
return info

src/renderer/main.js

+2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
faList,
7171
faLocationDot,
7272
faLock,
73+
faMoneyCheckDollar,
7374
faNetworkWired,
7475
faNewspaper,
7576
faPalette,
@@ -182,6 +183,7 @@ library.add(
182183
faList,
183184
faLocationDot,
184185
faLock,
186+
faMoneyCheckDollar,
185187
faNetworkWired,
186188
faNewspaper,
187189
faPalette,

src/renderer/views/Watch/Watch.js

+58-27
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import packageDetails from '../../../../package.json'
1515
import {
1616
buildVTTFileLocally,
1717
copyToClipboard,
18+
extractNumberFromString,
1819
formatDurationAsTimestamp,
1920
formatNumber,
2021
showToast
@@ -356,31 +357,12 @@ export default defineComponent({
356357
return
357358
}
358359

359-
const playabilityStatus = result.playability_status
360-
361-
// The apostrophe is intentionally that one (char code 8217), because that is the one YouTube uses
362-
const BOT_MESSAGE = 'Sign in to confirm you’re not a bot'
363-
364-
if (playabilityStatus.status === 'UNPLAYABLE' || (playabilityStatus.status === 'LOGIN_REQUIRED' && playabilityStatus.reason === BOT_MESSAGE)) {
365-
if (playabilityStatus.reason === BOT_MESSAGE) {
366-
throw new Error(this.$t('Video.IP block'))
367-
}
368-
369-
let errorText = `[${playabilityStatus.status}] ${playabilityStatus.reason}`
370-
371-
if (playabilityStatus.error_screen) {
372-
errorText += `: ${playabilityStatus.error_screen.subreason.text}`
373-
}
374-
375-
throw new Error(errorText)
376-
}
377-
378360
// extract localised title first and fall back to the not localised one
379361
this.videoTitle = result.primary_info?.title.text ?? result.basic_info.title
380-
this.videoViewCount = result.basic_info.view_count
362+
this.videoViewCount = result.basic_info.view_count ?? extractNumberFromString(result.primary_info.view_count.text)
381363

382-
this.channelId = result.basic_info.channel_id
383-
this.channelName = result.basic_info.author
364+
this.channelId = result.basic_info.channel_id ?? result.secondary_info.owner?.author.id
365+
this.channelName = result.basic_info.author ?? result.secondary_info.owner?.author.name
384366

385367
if (result.secondary_info.owner?.author) {
386368
this.channelThumbnail = result.secondary_info.owner.author.best_thumbnail?.url ?? ''
@@ -396,8 +378,13 @@ export default defineComponent({
396378
channelId: this.channelId
397379
})
398380

399-
// `result.page[0].microformat.publish_date` example value: `2023-08-12T08:59:59-07:00`
400-
this.videoPublished = new Date(result.page[0].microformat.publish_date).getTime()
381+
if (result.page[0].microformat?.publish_date) {
382+
// `result.page[0].microformat.publish_date` example value: `2023-08-12T08:59:59-07:00`
383+
this.videoPublished = new Date(result.page[0].microformat.publish_date).getTime()
384+
} else {
385+
// text date Jan 1, 2000, not as accurate but better than nothing
386+
this.videoPublished = new Date(result.primary_info.published).getTime()
387+
}
401388

402389
if (result.secondary_info?.description.runs) {
403390
try {
@@ -421,7 +408,7 @@ export default defineComponent({
421408
this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxres3.jpg`
422409
break
423410
default:
424-
this.thumbnail = result.basic_info.thumbnail[0].url
411+
this.thumbnail = result.basic_info.thumbnail?.[0].url ?? `https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg`
425412
break
426413
}
427414

@@ -465,7 +452,7 @@ export default defineComponent({
465452
})
466453
}
467454
} else {
468-
chapters = this.extractChaptersFromDescription(result.basic_info.short_description)
455+
chapters = this.extractChaptersFromDescription(result.basic_info.short_description ?? result.secondary_info.description.text)
469456
}
470457

471458
if (chapters.length > 0) {
@@ -481,6 +468,51 @@ export default defineComponent({
481468

482469
this.videoChapters = chapters
483470

471+
const playabilityStatus = result.playability_status
472+
473+
// The apostrophe is intentionally that one (char code 8217), because that is the one YouTube uses
474+
const BOT_MESSAGE = 'Sign in to confirm you’re not a bot'
475+
476+
if (playabilityStatus.status === 'UNPLAYABLE' || playabilityStatus.status === 'LOGIN_REQUIRED') {
477+
if (playabilityStatus.error_screen?.offer_id === 'sponsors_only_video') {
478+
// Members-only videos can only be watched while logged into a Google account that is a paid channel member
479+
// so there is no point trying any other backends as it will always fail
480+
this.errorMessage = this.$t('Video.MembersOnly')
481+
this.customErrorIcon = ['fas', 'money-check-dollar']
482+
this.isLoading = false
483+
this.updateTitle()
484+
return
485+
} else if (playabilityStatus.reason === 'Sign in to confirm your age' || (result.has_trailer && result.getTrailerInfo() === null)) {
486+
// Age-restricted videos can only be watched while logged into a Google account that is age-verified
487+
// so there is no point trying any other backends as it will always fail
488+
this.errorMessage = this.$t('Video.AgeRestricted')
489+
this.isLoading = false
490+
this.updateTitle()
491+
return
492+
}
493+
494+
let errorText
495+
496+
if (playabilityStatus.reason === BOT_MESSAGE || playabilityStatus.reason === 'Please sign in') {
497+
errorText = this.$t('Video.IP block')
498+
} else {
499+
errorText = `[${playabilityStatus.status}] ${playabilityStatus.reason}`
500+
501+
if (playabilityStatus.error_screen?.subreason) {
502+
errorText += `: ${playabilityStatus.error_screen.subreason.text}`
503+
}
504+
}
505+
506+
if (this.backendFallback) {
507+
throw new Error(errorText)
508+
} else {
509+
this.errorMessage = errorText
510+
this.isLoading = false
511+
this.updateTitle()
512+
return
513+
}
514+
}
515+
484516
if (!this.hideLiveChat && this.isLive && result.livechat) {
485517
this.liveChat = result.getLiveChat()
486518
} else {
@@ -700,7 +732,6 @@ export default defineComponent({
700732
}
701733
}
702734

703-
// this.errorMessage = 'Test error message'
704735
this.isLoading = false
705736
this.updateTitle()
706737
} catch (err) {

static/locales/en-US.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,8 @@ Channel:
765765
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.
766766
Video:
767767
IP block: 'YouTube has blocked your IP address from watching videos. Please try switching to a different VPN or proxy.'
768+
MembersOnly: Members-only videos cannot be watched with FreeTube as they require Google login and paid membership to the uploader's channel.
769+
AgeRestricted: Age-restricted videos cannot be watched with FreeTube as they require Google login and using an age-verified YouTube account.
768770
More Options: More Options
769771
Mark As Watched: Mark As Watched
770772
Remove From History: Remove From History

0 commit comments

Comments
 (0)