From 313ecbd8022672a27cd9cf5bcf62210c991b6cb9 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:13:22 -0500 Subject: [PATCH 1/7] Add support for viewing movie trailers with local api --- src/renderer/helpers/api/local.js | 11 +++++---- src/renderer/views/Watch/Watch.js | 39 +++++++++++++++++++------------ 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index dfc84330d2aa2..9df23f3a8e56e 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -516,7 +516,7 @@ function handleSearchResponse(response) { const results = response.results .filter((item) => { - return item.type === 'Video' || item.type === 'Channel' || item.type === 'Playlist' || item.type === 'HashtagTile' + return item.type === 'Video' || item.type === 'Channel' || item.type === 'Playlist' || item.type === 'HashtagTile' || item.type === 'Movie' }) .map((item) => parseListItem(item)) @@ -609,8 +609,8 @@ export function parseLocalListVideo(video) { author: video.author.name, authorId: video.author.id, description: video.description, - viewCount: extractNumberFromString(video.view_count.text), - publishedText: video.published.isEmpty() ? null : video.published.text, + viewCount: video.view_count == null ? null : extractNumberFromString(video.view_count.text), + publishedText: (video.published == null || video.published.isEmpty()) ? null : video.published.text, lengthSeconds: isNaN(video.duration.seconds) ? '' : video.duration.seconds, liveNow: video.is_live, isUpcoming: video.is_upcoming || video.is_premiere, @@ -623,6 +623,7 @@ export function parseLocalListVideo(video) { */ function parseListItem(item) { switch (item.type) { + case 'Movie': case 'Video': return parseLocalListVideo(item) case 'Channel': { @@ -689,8 +690,8 @@ export function parseLocalWatchNextVideo(video) { title: video.title.text, author: video.author.name, authorId: video.author.id, - viewCount: extractNumberFromString(video.view_count.text), - publishedText: video.published.isEmpty() ? null : video.published.text, + viewCount: video.view_count == null ? null : extractNumberFromString(video.view_count.text), + publishedText: (video.published == null || video.published.isEmpty()) ? null : video.published.text, lengthSeconds: isNaN(video.duration.seconds) ? '' : video.duration.seconds, liveNow: video.is_live, isUpcoming: video.is_premiere diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 7a8412793d1a6..1854541295072 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -274,11 +274,12 @@ export default defineComponent({ try { let result = await getLocalVideoInfo(this.videoId) + let trailerResult = null this.isFamilyFriendly = result.basic_info.is_family_safe this.recommendedVideos = result.watch_next_feed - ?.filter((item) => item.type === 'CompactVideo') + ?.filter((item) => item.type === 'CompactVideo' || item.type === 'CompactMovie') .map(parseLocalWatchNextVideo) ?? [] if (this.showFamilyFriendlyOnly && !this.isFamilyFriendly) { @@ -300,7 +301,12 @@ export default defineComponent({ * @type {import ('youtubei.js').YTNodes.PlayerErrorMessage} */ const errorScreen = playabilityStatus.error_screen - throw new Error(`[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`) + + if (result.has_trailer) { + trailerResult = result.getTrailerInfo() + } else { + throw new Error(`[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`) + } } // extract localised title first and fall back to the not localised one @@ -430,6 +436,8 @@ export default defineComponent({ result = bypassedResult } + const streamingData = trailerResult?.streaming_data ?? result.streaming_data + if ((this.isLive || this.isPostLiveDvr) && !this.isUpcoming) { try { const formats = await getFormatsFromHLSManifest(result.streaming_data.hls_manifest_url) @@ -518,17 +526,17 @@ export default defineComponent({ this.upcomingTimeLeft = null } } else { - this.videoLengthSeconds = result.basic_info.duration - if (result.streaming_data) { - if (result.streaming_data.formats.length > 0) { - this.videoSourceList = result.streaming_data.formats.map(mapLocalFormat).reverse() + this.videoLengthSeconds = trailerResult?.basic_info?.duration ?? result.basic_info.duration + if (streamingData) { + if (streamingData.formats.length > 0) { + this.videoSourceList = streamingData.formats.map(mapLocalFormat).reverse() } else { - this.videoSourceList = filterLocalFormats(result.streaming_data.adaptive_formats, this.allowDashAv1Formats).map(mapLocalFormat).reverse() + this.videoSourceList = filterLocalFormats(streamingData.adaptive_formats, this.allowDashAv1Formats).map(mapLocalFormat).reverse() } this.adaptiveFormats = this.videoSourceList /** @type {import('../../helpers/api/local').LocalFormat[]} */ - const formats = [...result.streaming_data.formats, ...result.streaming_data.adaptive_formats] + const formats = [...streamingData.formats, ...streamingData.adaptive_formats] this.downloadLinks = formats.map((format) => { const qualityLabel = format.quality_label ?? format.bitrate const fps = format.fps ? `${format.fps}fps` : 'kbps' @@ -549,8 +557,9 @@ export default defineComponent({ } }) - if (result.captions) { - const captionTracks = result.captions.caption_tracks.map((caption) => { + const captions = trailerResult?.captions ?? result.captions + if (captions) { + const captionTracks = captions.caption_tracks.map((caption) => { return { url: caption.base_url, label: caption.name.text, @@ -593,8 +602,8 @@ export default defineComponent({ return } - if (result.streaming_data?.adaptive_formats.length > 0) { - const audioFormats = result.streaming_data.adaptive_formats.filter((format) => { + if (streamingData?.adaptive_formats.length > 0) { + const audioFormats = streamingData.adaptive_formats.filter((format) => { return format.has_audio }) @@ -613,7 +622,7 @@ export default defineComponent({ } // we need to alter the result object so the toDash function uses the filtered formats too - result.streaming_data.adaptive_formats = filterLocalFormats(result.streaming_data.adaptive_formats, this.allowDashAv1Formats) + streamingData.adaptive_formats = filterLocalFormats(streamingData.adaptive_formats, this.allowDashAv1Formats) // When `this.proxyVideos` is true // It's possible that the Invidious instance used, only supports a subset of the formats from Local API @@ -623,8 +632,8 @@ export default defineComponent({ this.adaptiveFormats = await this.getAdaptiveFormatsInvidious() this.dashSrc = await this.createInvidiousDashManifest() } else { - this.adaptiveFormats = result.streaming_data.adaptive_formats.map(mapLocalFormat) - this.dashSrc = await this.createLocalDashManifest(result) + this.adaptiveFormats = streamingData.adaptive_formats.map(mapLocalFormat) + this.dashSrc = await this.createLocalDashManifest(trailerResult ?? result) } if (this.activeFormat === 'audio') { From cbb0776f7d38d6d1cf77586573eb30f4e44b3d1a Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:31:22 -0500 Subject: [PATCH 2/7] add support for age restricted trailers --- src/renderer/views/Watch/Watch.js | 38 +++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 1854541295072..1d349e44207b1 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -274,7 +274,6 @@ export default defineComponent({ try { let result = await getLocalVideoInfo(this.videoId) - let trailerResult = null this.isFamilyFriendly = result.basic_info.is_family_safe @@ -290,10 +289,26 @@ export default defineComponent({ let playabilityStatus = result.playability_status let bypassedResult = null - if (playabilityStatus.status === 'LOGIN_REQUIRED') { + let streamingVideoId = this.videoId + let trailerIsNull = false + if (playabilityStatus.status === 'UNPLAYABLE' && result.has_trailer) { + bypassedResult = result.getTrailerInfo() + /** + * @type {import ('youtubei.js').YTNodes.PlayerLegacyDesktopYpcTrailer} + */ + const trailerScreen = result.playability_status.error_screen + streamingVideoId = trailerScreen.video_id + // if the trailer is null then it is likely age restricted. + trailerIsNull = bypassedResult == null + if (!trailerIsNull) { + playabilityStatus = bypassedResult.playability_status + } + } + + if (playabilityStatus.status === 'LOGIN_REQUIRED' || trailerIsNull) { // try to bypass the age restriction - bypassedResult = await getLocalVideoInfo(this.videoId, true) - playabilityStatus = result.playability_status + bypassedResult = await getLocalVideoInfo(streamingVideoId, true) + playabilityStatus = bypassedResult.playability_status } if (playabilityStatus.status === 'UNPLAYABLE') { @@ -301,12 +316,7 @@ export default defineComponent({ * @type {import ('youtubei.js').YTNodes.PlayerErrorMessage} */ const errorScreen = playabilityStatus.error_screen - - if (result.has_trailer) { - trailerResult = result.getTrailerInfo() - } else { - throw new Error(`[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`) - } + throw new Error(`[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`) } // extract localised title first and fall back to the not localised one @@ -436,7 +446,7 @@ export default defineComponent({ result = bypassedResult } - const streamingData = trailerResult?.streaming_data ?? result.streaming_data + const streamingData = result.streaming_data if ((this.isLive || this.isPostLiveDvr) && !this.isUpcoming) { try { @@ -526,7 +536,7 @@ export default defineComponent({ this.upcomingTimeLeft = null } } else { - this.videoLengthSeconds = trailerResult?.basic_info?.duration ?? result.basic_info.duration + this.videoLengthSeconds = result.basic_info.duration if (streamingData) { if (streamingData.formats.length > 0) { this.videoSourceList = streamingData.formats.map(mapLocalFormat).reverse() @@ -557,7 +567,7 @@ export default defineComponent({ } }) - const captions = trailerResult?.captions ?? result.captions + const captions = result.captions if (captions) { const captionTracks = captions.caption_tracks.map((caption) => { return { @@ -633,7 +643,7 @@ export default defineComponent({ this.dashSrc = await this.createInvidiousDashManifest() } else { this.adaptiveFormats = streamingData.adaptive_formats.map(mapLocalFormat) - this.dashSrc = await this.createLocalDashManifest(trailerResult ?? result) + this.dashSrc = await this.createLocalDashManifest(result) } if (this.activeFormat === 'audio') { From 64f01b47f4a407bf8e5ded2d84750b272e607c57 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:57:17 -0500 Subject: [PATCH 3/7] remove unused changes --- src/renderer/views/Watch/Watch.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 1d349e44207b1..66841c654f3f4 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -446,8 +446,6 @@ export default defineComponent({ result = bypassedResult } - const streamingData = result.streaming_data - if ((this.isLive || this.isPostLiveDvr) && !this.isUpcoming) { try { const formats = await getFormatsFromHLSManifest(result.streaming_data.hls_manifest_url) @@ -537,16 +535,16 @@ export default defineComponent({ } } else { this.videoLengthSeconds = result.basic_info.duration - if (streamingData) { - if (streamingData.formats.length > 0) { - this.videoSourceList = streamingData.formats.map(mapLocalFormat).reverse() + if (result.streaming_data) { + if (result.streaming_data.formats.length > 0) { + this.videoSourceList = result.streaming_data.formats.map(mapLocalFormat).reverse() } else { - this.videoSourceList = filterLocalFormats(streamingData.adaptive_formats, this.allowDashAv1Formats).map(mapLocalFormat).reverse() + this.videoSourceList = filterLocalFormats(result.streaming_data.adaptive_formats, this.allowDashAv1Formats).map(mapLocalFormat).reverse() } this.adaptiveFormats = this.videoSourceList /** @type {import('../../helpers/api/local').LocalFormat[]} */ - const formats = [...streamingData.formats, ...streamingData.adaptive_formats] + const formats = [...result.streaming_data.formats, ...result.streaming_data.adaptive_formats] this.downloadLinks = formats.map((format) => { const qualityLabel = format.quality_label ?? format.bitrate const fps = format.fps ? `${format.fps}fps` : 'kbps' @@ -567,9 +565,8 @@ export default defineComponent({ } }) - const captions = result.captions - if (captions) { - const captionTracks = captions.caption_tracks.map((caption) => { + if (result.captions) { + const captionTracks = result.captions.caption_tracks.map((caption) => { return { url: caption.base_url, label: caption.name.text, @@ -612,8 +609,8 @@ export default defineComponent({ return } - if (streamingData?.adaptive_formats.length > 0) { - const audioFormats = streamingData.adaptive_formats.filter((format) => { + if (result.streaming_data?.adaptive_formats.length > 0) { + const audioFormats = result.streaming_data.adaptive_formats.filter((format) => { return format.has_audio }) @@ -632,7 +629,7 @@ export default defineComponent({ } // we need to alter the result object so the toDash function uses the filtered formats too - streamingData.adaptive_formats = filterLocalFormats(streamingData.adaptive_formats, this.allowDashAv1Formats) + result.streaming_data.adaptive_formats = filterLocalFormats(result.streaming_data.adaptive_formats, this.allowDashAv1Formats) // When `this.proxyVideos` is true // It's possible that the Invidious instance used, only supports a subset of the formats from Local API @@ -642,7 +639,7 @@ export default defineComponent({ this.adaptiveFormats = await this.getAdaptiveFormatsInvidious() this.dashSrc = await this.createInvidiousDashManifest() } else { - this.adaptiveFormats = streamingData.adaptive_formats.map(mapLocalFormat) + this.adaptiveFormats = result.streaming_data.adaptive_formats.map(mapLocalFormat) this.dashSrc = await this.createLocalDashManifest(result) } From 1dc2f33d0e5b6573985398856f0eab53d9582284 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 20 Dec 2023 23:20:28 -0500 Subject: [PATCH 4/7] always show trailer, regardless of video playability status --- src/renderer/views/Watch/Watch.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index c3472772024d1..42b41a86fde4d 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -297,7 +297,9 @@ export default defineComponent({ let bypassedResult = null let streamingVideoId = this.videoId let trailerIsNull = false - if (playabilityStatus.status === 'UNPLAYABLE' && result.has_trailer) { + + // if widevine support is added then we should check if playabilityStatus.status is UNPLAYABLE too + if (result.has_trailer) { bypassedResult = result.getTrailerInfo() /** * @type {import ('youtubei.js').YTNodes.PlayerLegacyDesktopYpcTrailer} From 68dd946c7ebfdee6b60224978e8a182b48d1e4d5 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 3 Jan 2024 23:09:57 -0500 Subject: [PATCH 5/7] Improve movie parsing logic Co-authored-by: absidue <48293849+absidue@users.noreply.github.com> --- .../ft-list-video/ft-list-video.vue | 5 +- src/renderer/helpers/api/local.js | 52 +++++++++++++------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/ft-list-video/ft-list-video.vue b/src/renderer/components/ft-list-video/ft-list-video.vue index 495b3464bffcf..17c5c81b092fb 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.vue +++ b/src/renderer/components/ft-list-video/ft-list-video.vue @@ -112,7 +112,10 @@ > {{ channelName }} -