Skip to content

Commit

Permalink
Handle unplayable content better with the local API (#5922)
Browse files Browse the repository at this point in the history
  • Loading branch information
absidue authored Oct 27, 2024
1 parent 999e48b commit fbef37d
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 65 deletions.
62 changes: 24 additions & 38 deletions src/renderer/helpers/api/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
faList,
faLocationDot,
faLock,
faMoneyCheckDollar,
faNetworkWired,
faNewspaper,
faPalette,
Expand Down Expand Up @@ -182,6 +183,7 @@ library.add(
faList,
faLocationDot,
faLock,
faMoneyCheckDollar,
faNetworkWired,
faNewspaper,
faPalette,
Expand Down
85 changes: 58 additions & 27 deletions src/renderer/views/Watch/Watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import packageDetails from '../../../../package.json'
import {
buildVTTFileLocally,
copyToClipboard,
extractNumberFromString,
formatDurationAsTimestamp,
formatNumber,
showToast
Expand Down Expand Up @@ -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 ?? ''
Expand All @@ -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 {
Expand All @@ -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
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -700,7 +732,6 @@ export default defineComponent({
}
}

// this.errorMessage = 'Test error message'
this.isLoading = false
this.updateTitle()
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions static/locales/en-US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit fbef37d

Please sign in to comment.