diff --git a/YoutubeConfig.json b/YoutubeConfig.json index 5fc0cf4c89bc8c04e648f3932f41b8d8ef03c062..d375d0e1648811c7f7ba55b584aaa1e5aa42b86e 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": 185, + "version": 186, "iconUrl": "./youtube.png", "id": "35ae969a-a7db-11ed-afa1-0242ac120002", diff --git a/YoutubeScript.js b/YoutubeScript.js index 3ff8b0a5b0808b8546c2ec2a99153b0475f54a21..83206b38212bc1ed5076fb246cf5356ac3b49a1a 100644 --- a/YoutubeScript.js +++ b/YoutubeScript.js @@ -110,6 +110,7 @@ var _prefetchHome = null; var _prefetchHomeAuth = null; var _prefetchHomeUsed = false; + function getClientContext(isAuth = false) { return (isAuth) ? _clientContextAuth : _clientContext; } @@ -346,7 +347,7 @@ source.getChannelTemplateByClaimMap = () => { 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) || REGEX_VIDEO_URL_CLIP.test(url) || REGEX_VIDEO_URL_EMBED.test(url); }; -source.getContentDetails = (url, useAuth) => { +source.getContentDetails = (url, useAuth, simplify) => { useAuth = !!_settings?.authDetails || !!useAuth; url = convertIfOtherUrl(url); @@ -365,7 +366,7 @@ source.getContentDetails = (url, useAuth) => { const batch = http.batch().GET(url, headersUsed, useLogin); - if(videoId && _settings["youtubeDislikes"]) + if(videoId && _settings["youtubeDislikes"] && !simplify) batch.GET(URL_YOUTUBE_DISLIKES + videoId, {}, false); const resps = batch.execute(); @@ -374,9 +375,39 @@ source.getContentDetails = (url, useAuth) => { throw new ScriptException("Failed to request page [" + resps[0].code + "]"); } - const html = resps[0].body;//requestPage(url); - const initialData = getInitialData(html); + let html = resps[0].body;//requestPage(url); + let initialData = getInitialData(html); let initialPlayerData = getInitialPlayerData(html); + let clientConfig = getClientConfig(html); + + let retryAttemptCount = 0; + let isValid = false; + while(!isValid && retryAttemptCount <= 3) { + const invalidExperiments = [51217102, 51217476]; + var invalidExperimentIndexes = invalidExperiments.map(x=>clientConfig.FEXP_EXPERIMENTS.indexOf(x)); + if(clientConfig.FEXP_EXPERIMENTS && invalidExperimentIndexes.filter(x=>x >= 0).length > 0) { + retryAttemptCount++; + log("DETECTED BLOCKING ATTEMPT [" + JSON.stringify(invalidExperimentIndexes) + "]"); + log("EXPIDS: " + JSON.stringify(clientConfig.FEXP_EXPERIMENTS)); + bridge.toast("Detected Youtube blocking attempt, bypassing.. (" + retryAttemptCount + ")"); + + resps[0] = http.GET(url, headersUsed, useLogin); + if(!resps[0].isOk) + throw new ScriptException("Failed to request page [" + resps[0].code + "]"); + throwIfCaptcha(resps[0]); + + html = resps[0].body;//requestPage(url); + initialData = getInitialData(html); + initialPlayerData = getInitialPlayerData(html); + clientConfig = getClientConfig(html); + continue; + } + + if(retryAttemptCount > 0) { + log("RESOLVED EXPIDS: " + JSON.stringify(clientConfig.FEXP_EXPERIMENTS)) + } + isValid = true; + } if(initialPlayerData?.playabilityStatus?.status == "UNPLAYABLE") throw new UnavailableException("Video unplayable"); @@ -449,7 +480,7 @@ source.getContentDetails = (url, useAuth) => { } } //Substitute HLS manifest from iOS - if(USE_IOS_FALLBACK && videoDetails.hls && videoDetails.hls.url) { + if(USE_IOS_FALLBACK && videoDetails.hls && videoDetails.hls.url && !simplify) { const iosData = requestIOSStreamingData(videoDetails.id.value); if(IS_TESTING) console.log("IOS Streaming Data", iosData); @@ -1511,29 +1542,34 @@ function removeQuery(urlPart) { //#region Objects class YTVideoSource extends VideoUrlRangeSource { - constructor(obj) { + constructor(obj, originalUrl) { super(obj); + this.originalUrl = originalUrl; } getRequestModifier() { - return new YTRequestModifier(); + return new YTRequestModifier(this.originalUrl); } } class YTAudioSource extends AudioUrlRangeSource { - constructor(obj) { + constructor(obj, originalUrl) { super(obj); + this.originalUrl = originalUrl; } getRequestModifier() { - return new YTRequestModifier(); + return new YTRequestModifier(this.originalUrl); } } class YTRequestModifier extends RequestModifier { - constructor() { + constructor(originalUrl) { super({ allowByteSkip: false }); this.requestNumber = 0; + this.originalUrl = originalUrl; + this.newUrl = null; + this.newUrlCount = 0; } /** @@ -1544,20 +1580,31 @@ class YTRequestModifier extends RequestModifier { */ modifyRequest(url, headers) { const u = new URL(url); + const actualUrl = (this.newUrl) ? new URL(this.newUrl) : u; const isVideoPlaybackUrl = u.pathname.startsWith('/videoplayback'); if (isVideoPlaybackUrl && !u.searchParams.has("rn")) { - u.searchParams.set("rn", this.requestNumber.toString()); + actualUrl.searchParams.set("rn", this.requestNumber.toString()); } this.requestNumber++; + if(this.newUrl) { + log("BYPASS: Using NewURL For sources"); + log("BYPASS: OldUrl: " + u.toString()); + log("BYPASS: NewUrl: " + actualUrl.toString()); + log("BYPASS: Headers: " + JSON.stringify(headers)); + } + + + let removedRangeHeader = undefined; if (headers["Range"] && !u.searchParams.has("range")) { let range = headers["Range"]; if (range.startsWith("bytes=")) { range = range.substring("bytes=".length); } + removedRangeHeader = headers["Range"]; delete headers["Range"]; - u.searchParams.set("range", range); + actualUrl.searchParams.set("range", range); } const c = u.searchParams.get("c"); @@ -1570,6 +1617,41 @@ class YTRequestModifier extends RequestModifier { } headers['TE'] = "trailers"; + + + //I hate this + //Workaround for seemingly active blocking + /* + const isValid = refetchClient.request("HEAD", actualUrl.toString(), headers); + if(isValid.code == 403 && this.newUrlCount < 3) { + const itag = actualUrl.searchParams.get("itag"); + bridge.toast("Youtube block detected (" + (this.newUrlCount + 1) + "), bypassing.."); + log("Detected 403, attempting bypass"); + try { + const newDetailsResp = source.getContentDetails(this.originalUrl, false, true); + if(newDetailsResp) { + let source = newDetailsResp.video.videoSources.find(x=>x.itagId == itag); + if(!source) + source = newDetailsResp.video.audioSources.find(x=>x.itagId == itag); + if(source) { + this.newUrl = source.url; + this.newUrlCount++; + this.requestNumber = 0; + log("Injecting new source url[" + source.name + "]: " + source.url); + bridge.toast("Injecting new source url"); + if(removedRangeHeader) + headers["Range"] = removedRangeHeader; + return this.modifyRequest(url, headers); + } + } + else + bridge.toast("Bypass failed, couldn't reload [" + newDetailsResp.code + "]"); + } + catch(ex) { + bridge.toast("Bypass failed\n" + ex); + } + } + */ if (c) { switch (c) { @@ -1586,7 +1668,7 @@ class YTRequestModifier extends RequestModifier { } return { - url: u.toString(), + url: actualUrl.toString(), headers: headers } } @@ -2708,7 +2790,7 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa initEnd: parseInt(y.initRange?.end), indexStart: parseInt(y.indexRange?.start), indexEnd: parseInt(y.indexRange?.end) - }); + }, contextData.url); }).filter(x=>x != null), initialPlayerData.streamingData.adaptiveFormats.filter(x=>x.mimeType.startsWith("audio/")).map(y=>{ const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1); @@ -2741,7 +2823,7 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa indexStart: parseInt(y.indexRange?.start), indexEnd: parseInt(y.indexRange?.end), audioChannels: y.audioChannels - }); + }, contextData.url); }).filter(x=>x!=null), ) : new VideoSourceDescriptor([]), subtitles: initialPlayerData