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 }}
-
+
+ {{ channelName }}
+
+
•
{{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js
index 1a0bcdb2f0a5a..21a4474d999b7 100644
--- a/src/renderer/helpers/api/local.js
+++ b/src/renderer/helpers/api/local.js
@@ -537,22 +537,44 @@ export function parseLocalPlaylistVideo(video) {
}
/**
- * @param {import('youtubei.js').YTNodes.Video} video
+ * @param {import('youtubei.js').YTNodes.Video | import('youtubei.js').YTNodes.Movie} item
*/
-export function parseLocalListVideo(video) {
- return {
- type: 'video',
- videoId: video.id,
- title: video.title.text,
- author: video.author.name,
- authorId: video.author.id,
- description: video.description,
- 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,
- premiereDate: video.upcoming
+export function parseLocalListVideo(item) {
+ if (item.type === 'Movie') {
+ /** @type {import('youtubei.js').YTNodes.Movie} */
+ const movie = item
+
+ return {
+ type: 'video',
+ videoId: movie.id,
+ title: movie.title.text,
+ author: movie.author.name,
+ authorId: movie.author.id !== 'N/A' ? movie.author.id : null,
+ description: movie.description_snippet?.text,
+ viewCount: null,
+ publishedText: null,
+ lengthSeconds: isNaN(movie.duration.seconds) ? '' : movie.duration.seconds,
+ liveNow: false,
+ isUpcoming: false,
+ premiereDate: null
+ }
+ } else {
+ /** @type {import('youtubei.js').YTNodes.Video} */
+ const video = item
+ return {
+ type: 'video',
+ videoId: video.id,
+ title: video.title.text,
+ author: video.author.name,
+ authorId: video.author.id,
+ description: video.description,
+ 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,
+ premiereDate: video.upcoming
+ }
}
}
From af737c22b26d99574d02b6cca6f3e5f1d6127729 Mon Sep 17 00:00:00 2001
From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
Date: Wed, 3 Jan 2024 23:11:46 -0500
Subject: [PATCH 6/7] check for null instead of truthy
---
src/renderer/components/ft-list-video/ft-list-video.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 17c5c81b092fb..d49e86b3d4430 100644
--- a/src/renderer/components/ft-list-video/ft-list-video.vue
+++ b/src/renderer/components/ft-list-video/ft-list-video.vue
@@ -115,7 +115,7 @@
{{ channelName }}
-
+
•
{{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
From 1f9ecfad3534b4710a89f52d227426f5c3fed59a Mon Sep 17 00:00:00 2001
From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
Date: Fri, 12 Jan 2024 12:19:09 -0500
Subject: [PATCH 7/7] Exclude unneeded properties
Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
---
src/renderer/helpers/api/local.js | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js
index 21a4474d999b7..9a7ec7acd98a0 100644
--- a/src/renderer/helpers/api/local.js
+++ b/src/renderer/helpers/api/local.js
@@ -551,12 +551,9 @@ export function parseLocalListVideo(item) {
author: movie.author.name,
authorId: movie.author.id !== 'N/A' ? movie.author.id : null,
description: movie.description_snippet?.text,
- viewCount: null,
- publishedText: null,
lengthSeconds: isNaN(movie.duration.seconds) ? '' : movie.duration.seconds,
liveNow: false,
isUpcoming: false,
- premiereDate: null
}
} else {
/** @type {import('youtubei.js').YTNodes.Video} */