diff --git a/YoutubeConfig.json b/YoutubeConfig.json index 79a07cab6d8947cf576efaa1b8fbaf5c0d24255d..43da9ba2af721abab12c03ecb0cdfd081fd8bfce 100644 --- a/YoutubeConfig.json +++ b/YoutubeConfig.json @@ -7,7 +7,7 @@ "sourceUrl": "https://plugins.grayjay.app/Youtube/YoutubeConfig.json", "repositoryUrl": "https://futo.org", "scriptUrl": "./YoutubeScript.js", - "version": 113, + "version": 118, "iconUrl": "./youtube.png", "id": "35ae969a-a7db-11ed-afa1-0242ac120002", diff --git a/YoutubeScript.js b/YoutubeScript.js index 8a3f7954cf3b496d8646d167671fc4f3743e5402..0c43e9fd1413b83df7c8d651a5fb19701d956e20 100644 --- a/YoutubeScript.js +++ b/YoutubeScript.js @@ -17,7 +17,7 @@ const URL_SUBSCRIPTIONS_M = "https://m.youtube.com/feed/subscriptions"; const URL_PLAYLIST = "https://youtube.com/playlist?list="; const URL_PLAYLISTS_M = "https://m.youtube.com/feed/library"; -const URL_LIVE_CHAT_HTML = "https://www.youtube.com/live_chat"; +const URL_LIVE_CHAT_HTML = "https://www.youtube.com/live_chat?v="; const URL_LIVE_CHAT = "https://www.youtube.com/youtubei/v1/live_chat/get_live_chat"; const URL_WATCHTIME = "https://www.youtube.com/api/stats/watchtime"; @@ -427,13 +427,29 @@ source.getContentDetails = (url, useAuth) => { return finalResult; }; +source.getLiveChatWindow = function(url) { + const id = extractVideoIDFromUrl(url); + if(!id) + throw new ScriptException("No valid id found"); + + const chatUrl = URL_LIVE_CHAT_HTML + id; + const chatHtmlResp = http.GET(chatUrl, {}, false); + if(!chatHtmlResp.isOk) + return null; + else { + return { + url: chatUrl, + removeElements: [ "yt-live-chat-header-renderer", "#ticker" ] + }; + } +} source.getLiveEvents = function(url) { const id = extractVideoIDFromUrl(url); if(!id) throw new ScriptException("No valid id found"); - const chatHtmlResp = http.GET(URL_LIVE_CHAT_HTML + "?v=" + id, {}, false); + const chatHtmlResp = http.GET(URL_LIVE_CHAT_HTML + id, {}, false); if(!chatHtmlResp.isOk) throw new ScriptException("Failed to get chat html"); const chatHtml = chatHtmlResp.body; @@ -473,6 +489,9 @@ class YoutubePlaybackTracker extends PlaybackTracker { this.watchUrl = playerData.playbackTracking?.videostatsWatchtimeUrl?.baseUrl; this.playbackUrl = playerData.playbackTracking?.videostatsPlaybackUrl?.baseUrl; + if(!this.playbackUrl || !this.watchUrl) + throw new ScriptException("Playback tracking unavailable"); + this.playbackUrlBase = this.playbackUrl.substring(0, this.playbackUrl.indexOf("?")); this.watchUrlBase = this.watchUrl.substring(0, this.watchUrl.indexOf("?")); this.watchParams = parseQueryString(this.watchUrl); @@ -1528,7 +1547,7 @@ function requestInitialData(url, useMobile = false, useAuth = false) { const initialData = getInitialData(html); return initialData; } - else throw new ScriptException("Failed to request page [" + resp.code + "]"); + else throw new ScriptException("Failed to request page [" + resp.code + "]\n" + url + "\n"); } function requestClientConfig(useMobile = false, useAuth = false) { let headers = { @@ -1754,9 +1773,13 @@ function extractChannelListSubMenuAvatarRenderer_AuthorLink(renderer) { * @returns {String[]} Urls */ function extractChannelListSubMenuAvatarRenderer_URL(renderer) { - const url = renderer?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl ? + const canonicalUrl = renderer?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl ? URL_BASE + renderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl : null; + const idUrl = renderer?.navigationEndpoint?.browseEndpoint?.browseId ? + URL_BASE + "/channel/" + renderer.navigationEndpoint.browseEndpoint.browseId : + null; + const url = idUrl ?? canonicalUrl; if(!url) return null; else @@ -1848,6 +1871,13 @@ function extractChannel_PlatformChannel(initialData, sourceUrl = null) { const bannerTargetWidth = 1080; const banner = (banners && banners.length > 0) ? banners.sort((a,b)=>Math.abs(a.width - bannerTargetWidth) - Math.abs(b.width - bannerTargetWidth))[0] : { url: "" }; + const idUrl = headerRenderer?.navigationEndpoint?.browseEndpoint?.browseId ? + URL_BASE + "/channel/" + headerRenderer.navigationEndpoint.browseEndpoint.browseId : + null; + const canonicalUrl = headerRenderer?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl ? + URL_BASE + headerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl : + null; + return new PlatformChannel({ id: new PlatformID(PLATFORM, headerRenderer.channelId, config.id, PLATFORM_CLAIMTYPE), name: headerRenderer.title ?? "", @@ -1855,7 +1885,8 @@ function extractChannel_PlatformChannel(initialData, sourceUrl = null) { banner: banner.url, subscribers: Math.max(0, extractHumanNumber_Integer(extractText_String(headerRenderer.subscriberCountText))), description: "", - url: URL_BASE + headerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl, + url: idUrl, + urlAlternatives: [idUrl, canonicalUrl], links: {} }); } @@ -2093,10 +2124,11 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa if(!video.datetime || video.datetime <= 0) { let date = 0; + if (date <= 0 && renderer.relativeDateText?.simpleText) + date = extractAgoText_Timestamp(renderer.relativeDateText.simpleText); if(date <= 0 && renderer.dateText?.simpleText) date = extractDate_Timestamp(renderer.dateText.simpleText); - if(date <= 0 && renderer.relativeDateText?.simpleText) - date = extractAgoText_Timestamp(renderer.relativeDateText.simpleText); + video.datetime = date; } }, @@ -2150,9 +2182,10 @@ function extractVideoOwnerRenderer_AuthorLink(renderer) { if(renderer.subscriberCountText) subscribers = extractHumanNumber_Integer(extractText_String(renderer.subscriberCountText)); - return new PlatformAuthorLink(new PlatformID(PLATFORM, renderer?.navigationEndpoint?.browseEndpoint?.browseId, config.id, PLATFORM_CLAIMTYPE), + const id = renderer?.navigationEndpoint?.browseEndpoint?.browseId; + return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), extractRuns_String(renderer.title.runs), - extractRuns_Url(renderer.title.runs), + (!id) ? extractRuns_Url(renderer.title.runs) : URL_BASE + "/channel/" + id, bestThumbnail, subscribers); } @@ -2537,6 +2570,10 @@ function extractVideoWithContextRenderer_Video(videoRenderer, contextData) { x.thumbnailOverlayTimeStatusRenderer?.accessibility?.accessibilityData?.label == "LIVE"); let isLive = liveBadges != null && liveBadges.length > 0; + let plannedDate = null; + if(videoRenderer.upcomingEventData?.startTime) + plannedDate = parseInt(videoRenderer.upcomingEventData.startTime); + //if(!isLive && !videoRenderer.publishedTimeText?.simpleText) // return null; //Not a normal video @@ -2557,20 +2594,20 @@ function extractVideoWithContextRenderer_Video(videoRenderer, contextData) { const title = (videoRenderer.headline) ? extractText_String(videoRenderer.headline) : extractText_String(videoRenderer.title); - if(isLive) + if (isLive) { return new PlatformVideo({ id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id), name: title, thumbnails: extractThumbnail_Thumbnails(videoRenderer.thumbnail), author: author, - uploadDate: parseInt(new Date().getTime()/1000), + uploadDate: plannedDate ?? parseInt(new Date().getTime() / 1000), duration: 0, viewCount: viewCount, url: URL_BASE + "/watch?v=" + videoRenderer.videoId, isLive: true, extractType: "VideoWithContext" }); - else + } else { return new PlatformVideo({ id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id), name: title, @@ -2583,6 +2620,7 @@ function extractVideoWithContextRenderer_Video(videoRenderer, contextData) { isLive: false, extractType: "VideoWithContext" }); + } } function extractVideoRenderer_Video(videoRenderer, contextData) { @@ -2733,7 +2771,7 @@ function extractVideoWithContextRenderer_AuthorLink(videoRenderer) { const thumbUrl = channelThumbs && channelThumbs.length > 0 ? channelThumbs[0].url : null; let channelUrl = videoRenderer.channelThumbnail?.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl; if(channelUrl) channelUrl = URL_BASE + channelUrl; - + if (id) channelUrl = URL_BASE + "/channel/" + id; return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl); } function extractVideoRenderer_AuthorLink(videoRenderer) { @@ -2741,7 +2779,7 @@ function extractVideoRenderer_AuthorLink(videoRenderer) { const name = extractRuns_String(videoRenderer.ownerText.runs); const channelIcon = videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer; const thumbUrl = channelIcon.thumbnail.thumbnails[0].url; - const channelUrl = extractRuns_Url(videoRenderer.ownerText.runs); + const channelUrl = (!id) ? extractRuns_Url(videoRenderer.ownerText.runs) : URL_BASE + "/channel/" + id; return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl); } @@ -2828,7 +2866,9 @@ function extractNavigationEndpoint_Url(navEndpoint, baseUrl) { if(!baseUrl) baseUrl = URL_BASE; if(!navEndpoint) - return null; + return null; + if(navEndpoint?.browseEndpoint?.browseId && navEndpoint?.browseEndpoint?.canonicalBaseUrl && navEndpoint.browseEndpoint.canonicalBaseUrl.startsWith("/@")) + return baseUrl + "/channel/" + navEndpoint?.browseEndpoint?.browseId; if(navEndpoint?.browseEndpoint?.canonicalBaseUrl) return baseUrl + navEndpoint?.browseEndpoint?.canonicalBaseUrl; if(navEndpoint.commandMetadata?.webCommandMetadata?.url) diff --git a/diff b/diff new file mode 100644 index 0000000000000000000000000000000000000000..7f1af63500583e4abe468a1fe71559ffba694f78 --- /dev/null +++ b/diff @@ -0,0 +1,235 @@ +--- YoutubeScript.js 2023-09-19 14:30:36.635400711 +0200 ++++ ../../old/Youtube/YoutubeScript.js 2023-09-19 13:50:05.107878724 +0200 +@@ -32,7 +32,6 @@ + const CIPHER_TEST_SUFFIX = "/player_ias.vflset/en_US/base.js"; + + const PLATFORM = "YouTube"; +-const PLATFORM_CLAIMTYPE = 2; + + const BROWSE_TRENDING = "FEtrending"; + const BROWSE_WHAT_TO_WATCH = "FEwhat_to_watch"; +@@ -278,27 +277,6 @@ + throw new ScriptException("No search tab found"); + } + +-source.getChannelUrlByClaim = (claimType, claimValues) => { +- const values = claimValues.values(); +- if (values.length == 0) +- return null; +- const atName = values.find(x => x.startsWith("@")); +- if (atName) +- return URL_BASE + "/" + atName; +- else +- return URL_BASE + "/channel/" + values[0]; +-}; +-source.getChannelTemplateByClaimMap = () => { +- return { +- //Youtube +- 2: { +- 0: URL_BASE + "/{{CLAIMVALUE}}", +- 1: URL_BASE + "/channel/{{CLAIMVALUE}}", +- } +- }; +-}; +- +- + //Video + source.isContentDetailsUrl = (url) => { + return REGEX_VIDEO_URL_DESKTOP.test(url) || REGEX_VIDEO_URL_SHARE.test(url) || REGEX_VIDEO_URL_SHARE_LIVE.test(url) || REGEX_VIDEO_URL_SHORT.test(url); +@@ -427,6 +405,7 @@ + + return finalResult; + }; ++ + source.getLiveChatWindow = function(url) { + const id = extractVideoIDFromUrl(url); + if(!id) +@@ -491,7 +470,6 @@ + this.playbackUrl = playerData.playbackTracking?.videostatsPlaybackUrl?.baseUrl; + if(!this.playbackUrl || !this.watchUrl) + throw new ScriptException("Playback tracking unavailable"); +- + this.playbackUrlBase = this.playbackUrl.substring(0, this.playbackUrl.indexOf("?")); + this.watchUrlBase = this.watchUrl.substring(0, this.watchUrl.indexOf("?")); + this.watchParams = parseQueryString(this.watchUrl); +@@ -672,7 +650,7 @@ + const initialData = requestInitialData(url, false, useAuth); + const channel = extractChannel_PlatformChannel(initialData, url); + const contextData = { +- authorLink: new PlatformAuthorLink(new PlatformID(PLATFORM, channel.id.value, config.id, PLATFORM_CLAIMTYPE), channel.name, channel.url, channel.thumbnail) ++ authorLink: new PlatformAuthorLink(new PlatformID(PLATFORM, channel.id.value, config.id), channel.name, channel.url, channel.thumbnail) + }; + const tabs = extractPage_Tabs(initialData, contextData); + +@@ -1766,7 +1744,7 @@ + if(!url || !name) + return null; + else +- return new PlatformAuthorLink(new PlatformID(PLATFORM, null, config?.id, PLATFORM_CLAIMTYPE), name, url, thumbnail); ++ return new PlatformAuthorLink(new PlatformID(PLATFORM, null, config?.id), name, url, thumbnail); + } + /** + * Extract Subscription channels from a submenu obtained from subscriptionsPage +@@ -1828,7 +1806,7 @@ + const url = guideEntryRenderer.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl ? + URL_BASE + guideEntryRenderer.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl : null; + +- return new PlatformAuthorLink(new PlatformID(PLATFORM, null, config.id, PLATFORM_CLAIMTYPE), name, url, thumbnail); ++ return new PlatformAuthorLink(new PlatformID(PLATFORM, null, config.id), name, url, thumbnail); + } + + /** +@@ -1870,7 +1848,7 @@ + const banners = headerRenderer.banner?.thumbnails; + const bannerTargetWidth = 1080; + const banner = (banners && banners.length > 0) ? banners.sort((a,b)=>Math.abs(a.width - bannerTargetWidth) - Math.abs(b.width - bannerTargetWidth))[0] : { url: "" }; +- ++ + const idUrl = headerRenderer?.navigationEndpoint?.browseEndpoint?.browseId ? + URL_BASE + "/channel/" + headerRenderer.navigationEndpoint.browseEndpoint.browseId : + null; +@@ -1879,7 +1857,7 @@ + null; + + return new PlatformChannel({ +- id: new PlatformID(PLATFORM, headerRenderer.channelId, config.id, PLATFORM_CLAIMTYPE), ++ id: new PlatformID(PLATFORM, headerRenderer.channelId, config.id), + name: headerRenderer.title ?? "", + thumbnail: thumbnail.url, + banner: banner.url, +@@ -1943,7 +1921,7 @@ + id: new PlatformID(PLATFORM, videoDetails.videoId, config.id), + name: videoDetails.title, + thumbnails: new Thumbnails(videoDetails.thumbnail?.thumbnails.map(x=>new Thumbnail(x.url, x.height)) ?? []), +- author: new PlatformAuthorLink(new PlatformID(PLATFORM, videoDetails.channelId, config.id, PLATFORM_CLAIMTYPE), videoDetails.author, URL_BASE + "/channel/" + videoDetails.channelId, null), ++ author: new PlatformAuthorLink(new PlatformID(PLATFORM, videoDetails.channelId, config.id), videoDetails.author, URL_BASE + "/channel/" + videoDetails.channelId, null), + duration: parseInt(videoDetails.lengthSeconds), + viewCount: parseInt(videoDetails.viewCount), + url: contextData.url, +@@ -2124,11 +2102,10 @@ + if(!video.datetime || video.datetime <= 0) { + let date = 0; + +- if (date <= 0 && renderer.relativeDateText?.simpleText) ++ if(date <= 0 && renderer.relativeDateText?.simpleText) + date = extractAgoText_Timestamp(renderer.relativeDateText.simpleText); + if(date <= 0 && renderer.dateText?.simpleText) + date = extractDate_Timestamp(renderer.dateText.simpleText); +- + video.datetime = date; + } + }, +@@ -2183,7 +2160,8 @@ + subscribers = extractHumanNumber_Integer(extractText_String(renderer.subscriberCountText)); + + const id = renderer?.navigationEndpoint?.browseEndpoint?.browseId; +- return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), ++ ++ return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id), + extractRuns_String(renderer.title.runs), + (!id) ? extractRuns_Url(renderer.title.runs) : URL_BASE + "/channel/" + id, + bestThumbnail, +@@ -2570,10 +2548,10 @@ + x.thumbnailOverlayTimeStatusRenderer?.accessibility?.accessibilityData?.label == "LIVE"); + let isLive = liveBadges != null && liveBadges.length > 0; + ++ + let plannedDate = null; + if(videoRenderer.upcomingEventData?.startTime) + plannedDate = parseInt(videoRenderer.upcomingEventData.startTime); +- + //if(!isLive && !videoRenderer.publishedTimeText?.simpleText) + // return null; //Not a normal video + +@@ -2594,19 +2572,21 @@ + + const title = (videoRenderer.headline) ? extractText_String(videoRenderer.headline) : extractText_String(videoRenderer.title); + +- if (isLive) { ++ if(isLive) { + return new PlatformVideo({ + id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id), + name: title, + thumbnails: extractThumbnail_Thumbnails(videoRenderer.thumbnail), + author: author, +- uploadDate: plannedDate ?? parseInt(new Date().getTime()/1000), duration: 0, ++ uploadDate: plannedDate ?? parseInt(new Date().getTime()/1000), ++ duration: 0, + viewCount: viewCount, + url: URL_BASE + "/watch?v=" + videoRenderer.videoId, + isLive: true, + extractType: "VideoWithContext" + }); +- } else { ++ } ++ else + return new PlatformVideo({ + id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id), + name: title, +@@ -2619,7 +2599,6 @@ + isLive: false, + extractType: "VideoWithContext" + }); +- } + } + function extractVideoRenderer_Video(videoRenderer, contextData) { + +@@ -2733,7 +2712,7 @@ + + const subscribers = extractHumanNumber_Integer(extractText_String(channelRenderer.videoCountText)); + +- return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl, subscribers); ++ return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id), name, channelUrl, thumbUrl, subscribers); + } + + function extractRuns_AuthorLink(runs) { +@@ -2745,7 +2724,7 @@ + const channelUrl = extractNavigationEndpoint_Url(runs[0]?.navigationEndpoint); + const thumbUrl = null; + +- return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl ?? ""); ++ return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id), name, channelUrl, thumbUrl ?? ""); + } + + function extractThumbnail_Thumbnails(thumbnail) { +@@ -2770,8 +2749,9 @@ + const thumbUrl = channelThumbs && channelThumbs.length > 0 ? channelThumbs[0].url : null; + let channelUrl = videoRenderer.channelThumbnail?.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl; + if(channelUrl) channelUrl = URL_BASE + channelUrl; +- if (id) channelUrl = URL_BASE + "/channel/" + id; +- return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl); ++ if(id) channelUrl = URL_BASE + "/channel/" + id; ++ ++ return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id), name, channelUrl, thumbUrl); + } + function extractVideoRenderer_AuthorLink(videoRenderer) { + const id = videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.browseId; +@@ -2780,7 +2760,7 @@ + const thumbUrl = channelIcon.thumbnail.thumbnails[0].url; + const channelUrl = (!id) ? extractRuns_Url(videoRenderer.ownerText.runs) : URL_BASE + "/channel/" + id; + +- return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl); ++ return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id), name, channelUrl, thumbUrl); + } + function extractCommentRenderer_Comment(contextUrl, commentRenderer, replyCount, replyContinuation) { + const authorName = commentRenderer.authorText?.simpleText ?? ""; +@@ -2791,7 +2771,7 @@ + ); + return new YTComment({ + contextUrl: contextUrl, +- author: new PlatformAuthorLink(new PlatformID(PLATFORM, null, config.id, PLATFORM_CLAIMTYPE), authorName, URL_BASE + authorEndpoint, authorThumbnail), ++ author: new PlatformAuthorLink(new PlatformID(PLATFORM, null, config.id), authorName, URL_BASE + authorEndpoint, authorThumbnail), + message: extractRuns_String(commentRenderer.contentText?.runs) ?? "", + rating: new RatingLikes(commentRenderer?.voteCount?.simpleText ? extractHumanNumber_Integer(commentRenderer.voteCount.simpleText) : 0), + date: (commentRenderer.publishedTimeText?.runs ? extractAgoTextRuns_Timestamp(commentRenderer.publishedTimeText.runs) : 0), +@@ -2865,8 +2845,8 @@ + if(!baseUrl) + baseUrl = URL_BASE; + if(!navEndpoint) +- return null; +- if(navEndpoint?.browseEndpoint?.browseId && navEndpoint?.browseEndpoint?.canonicalBaseUrl && navEndpoint.browseEndpoint.canonicalBaseUrl.startsWith("/@")) ++ return null; ++ if(navEndpoint?.browseEndpoint?.browseId && navEndpoint?.browseEndpoint?.canonicalBaseUrl && navEndpoint.browseEndpoint.canonicalBaseUrl.startsWith("/@")); + return baseUrl + "/channel/" + navEndpoint?.browseEndpoint?.browseId; + if(navEndpoint?.browseEndpoint?.canonicalBaseUrl) + return baseUrl + navEndpoint?.browseEndpoint?.canonicalBaseUrl; diff --git a/twitch.png b/twitch.png new file mode 100644 index 0000000000000000000000000000000000000000..73aec0e61f1ae4794b3ac9bd5d0370300d74fd24 Binary files /dev/null and b/twitch.png differ