Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • videostreaming/plugins/youtube
1 result
Show changes
Commits on Source (21)
......@@ -8,7 +8,7 @@
"repositoryUrl": "https://futo.org",
"scriptUrl": "./YoutubeScript.js",
"version": 220,
"version": 231,
"iconUrl": "./youtube.png",
"id": "35ae969a-a7db-11ed-afa1-0242ac120002",
......@@ -71,6 +71,14 @@
"type": "Boolean",
"default": "false"
},
{
"variable": "channelRssOnly",
"name": "Only Use Channel RSS Feeds (Inferior)",
"description": "Exclusively use channel RSS feeds for channel content, may result in inferior results, and only recent videos. But may be faster and reduce rate limiting.",
"type": "Boolean",
"default": "false",
"warningDialog": "Using RSS feeds will have inferior results, and may add shorts in the channel videos and subscriptions.\n\nOld videos may also be unavailable."
},
{
"variable": "allowAgeRestricted",
"name": "Allow Age Restricted",
......@@ -92,6 +100,13 @@
"type": "Boolean",
"default": "false"
},
{
"variable": "useAggressiveUMPRecovery",
"name": "Use Aggressive UMP Recovery",
"description": "This feature allows UMP to refetch the entire page to recover from ip changes and such.",
"type": "Boolean",
"default": "true"
},
{
"variable": "showVerboseToasts",
"name": "Show Verbose Messages",
......@@ -203,6 +218,14 @@
"description": "These are settings not intended for most users, but may help development or power users.",
"type": "Header"
},
{
"variable": "allow_av1",
"name": "Allow AV1",
"description": "Adds AV1 option when available, MAY NOT BE SUPPORTED YET!",
"type": "Boolean",
"default": "false",
"warningDialog": "AV1 support might not work yet, this allows you to return the stream even if its not supported (for testing)"
},
{
"variable": "notify_cipher",
"name": "Show Cipher every Video",
......@@ -216,6 +239,13 @@
"description": "Shows a toast with the botguard token used changed",
"type": "Boolean",
"default": "false"
},
{
"variable": "notify_ump_recovery",
"name": "Show every time UMP disconnects",
"description": "Shows a toast whenever UMP goes into a reconnection mode",
"type": "Boolean",
"default": "false"
}
],
......@@ -241,5 +271,14 @@
},
"supportedClaimTypes": [2],
"primaryClaimFieldType": 1
"primaryClaimFieldType": 1,
"changelog": {
"227": [
"UMP MP4 support (fixes no audio or only 360p for older videos)",
"UMP Disconnect toasts now optional",
"Opt-in AV1 support"
]
}
}
......@@ -131,6 +131,11 @@ function getClientContext(isAuth = false) {
return (isAuth) ? _clientContextAuth : _clientContext;
}
var _setMetadata = false;
source.enableMetadata = function() {
_setMetadata = true;
}
//#region Source Methods
source.setSettings = function(settings) {
_settings = settings;
......@@ -376,10 +381,12 @@ if(false && (bridge.buildSpecVersion ?? 1) > 1) {
//TODO: Implement more compact version using new api batch spec
}
else {
source.getContentDetails = (url, useAuth, simplify) => {
source.getContentDetails = (url, useAuth, simplify, forceUmp) => {
useAuth = !!_settings?.authDetails || !!useAuth;
console.clear(); //Temp fix for memory leaking
log("ABR Enabled: " + USE_ABR_VIDEOS);
const defaultUMP = USE_ABR_VIDEOS || forceUmp;
url = convertIfOtherUrl(url);
......@@ -407,10 +414,11 @@ else {
}
let batchIOS = -1;
if(USE_IOS_VIDEOS_FALLBACK) {
requestIOSStreamingData(videoId, batch);
/*
if(USE_IOS_VIDEOS_FALLBACK && !defaultUMP && !simplify) {
requestIOSStreamingData(videoId, batch, getBGDataFromClientConfig(clientConfig, usedLogin));
batchIOS = batchCounter++;
}
}*/
const resps = batch.execute();
......@@ -423,7 +431,15 @@ else {
let initialData = getInitialData(html);
let initialPlayerData = getInitialPlayerData(html);
let clientConfig = getClientConfig(html);
let usedLogin = useLogin && bridge.isLoggedIn();
/*
if(USE_IOS_VIDEOS_FALLBACK && !defaultUMP && !simplify) {
resps.push(requestIOSStreamingData(videoId, undefined, getBGDataFromClientConfig(clientConfig, usedLogin)));
batchIOS = batchCounter++;
}*/
let ageRestricted = initialPlayerData.playabilityStatus?.reason?.indexOf("your age") > 0 ?? false;
if (initialPlayerData.playabilityStatus?.status == "LOGIN_REQUIRED" && (bridge.isLoggedIn() || !ageRestricted)) {
if(!!_settings?.allowLoginFallback && !useLogin) {
......@@ -434,6 +450,7 @@ else {
initialData = getInitialData(html);
initialPlayerData = getInitialPlayerData(html);
clientConfig = getClientConfig(html);
usedLogin = true && bridge.isLoggedIn();
if (initialPlayerData.playabilityStatus?.status == "LOGIN_REQUIRED")
throw new ScriptLoginRequiredException("Login required\nReason: " + initialPlayerData?.playabilityStatus?.reason);
......@@ -516,9 +533,9 @@ else {
jsUrl: jsUrl
};
const videoDetails = extractVideoPage_VideoDetails(initialData, initialPlayerData, {
const videoDetails = extractVideoPage_VideoDetails(url, initialData, initialPlayerData, {
url: url
}, jsUrl, useLogin, USE_ABR_VIDEOS, clientConfig);
}, jsUrl, useLogin, defaultUMP, clientConfig, usedLogin);
if(videoDetails == null)
throw new UnavailableException("No video found");
......@@ -533,6 +550,9 @@ else {
throw new UnavailableException("No sources found");
}
let bgData = getBGDataFromClientConfig(clientConfig, usedLogin);
//Substitute Dash manifest from Android
if(USE_ANDROID_FALLBACK && videoDetails.dash && videoDetails.dash.url) {
const androidData = requestAndroidStreamingData(videoDetails.id.value);
......@@ -573,7 +593,7 @@ else {
else if(USE_IOS_VIDEOS_FALLBACK && !USE_ABR_VIDEOS && !simplify) {
const iosDataResp = (batchIOS > 0) ?
resps[batchIOS] :
requestIOSStreamingData(videoDetails.id.value);
requestIOSStreamingData(videoDetails.id.value, undefined, getBGDataFromClientConfig(clientConfig, usedLogin), usedLogin);
if(iosDataResp.isOk) {
const iosData = JSON.parse(iosDataResp.body);
if(IS_TESTING)
......@@ -589,14 +609,14 @@ else {
log("Failed to get iOS stream data, fallback to UMP")
if(!!_settings["showVerboseToasts"])
bridge.toast("Failed to get iOS stream data, fallback to UMP");
videoDetails.video = extractABR_VideoDescriptor(initialPlayerData, jsUrl) ?? new VideoSourceDescriptor([]);
videoDetails.video = extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig, url, usedLogin) ?? new VideoSourceDescriptor([]);
}
}
else {
log("Failed to get iOS stream data, fallback to UMP (" + iosDataResp?.code + ")")
if(!!_settings["showVerboseToasts"])
bridge.toast("Failed to get iOS stream data, fallback to UMP");
videoDetails.video = extractABR_VideoDescriptor(initialPlayerData, jsUrl) ?? new VideoSourceDescriptor([]);
videoDetails.video = extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig, url, usedLogin) ?? new VideoSourceDescriptor([]);
}
}
......@@ -616,6 +636,13 @@ else {
}
const finalResult = videoDetails;
finalResult.bgData = bgData;
if(_setMetadata) {
finalResult.metaData = {
"initialData": JSON.stringify(initialData)
}
}
finalResult.__initialData = initialData;
if(!!_settings["youtubeActivity"] && useLogin) {
finalResult.__playerData = initialPlayerData;
......@@ -634,6 +661,18 @@ else {
return source.getContentRecommendations(url, initialData);
}
if(false) {
const bgData = getBGDataFromClientConfig(clientConfig, usedLogin);
tryGetBotguard((bg)=>{
bg.getTokenOrCreate(bgData.visitorData, bgData.dataSyncId, (pot)=>{
console.log("Botguard Token to use:", pot);
for(let src of finalResult.video.videoSources)
src.pot = pot;
for(let src of finalResult.video.audioSources)
src.pot = pot;
}, bgData.visitorDataType);
});
}
return finalResult;
};
}
......@@ -1272,8 +1311,8 @@ source.getChannel = (url) => {
source.getChannelCapabilities = () => {
return {
types: [Type.Feed.Videos, Type.Feed.Streams],
sorts: [Type.Order.Chronological, "Popular"]
types: (!!_settings?.channelRssOnly) ? [Type.Feed.Mixed] : [Type.Feed.Videos, Type.Feed.Streams],
sorts: (!!_settings?.channelRssOnly) ? [Type.Order.Chronological] : [Type.Order.Chronological, "Popular"]//
};
}
function filterChannelUrl(url) {
......@@ -1300,6 +1339,12 @@ source.getChannelContents = (url, type, order, filters) => {
let targetTab = null;
url = filterChannelUrl(url);
if(!!_settings?.channelRssOnly) {
log("Using Channel RSS Only");
return new VideoPager(source.peekChannelContents(url, type, true), false);
}
log("GetChannelContents - " + type);
switch(type) {
case undefined:
......@@ -1387,10 +1432,15 @@ source.getChannelPlaylists = (url) => {
source.getPeekChannelTypes = () => {
return [Type.Feed.Videos, Type.Feed.Mixed];
}
source.peekChannelContents = function(url, type) {
source.peekChannelContents = function(url, type, allowChannelFetch) {
if(type != Type.Feed.Mixed && type != Type.Feed.Videos)
return [];
if(allowChannelFetch && !REGEX_VIDEO_CHANNEL_URL.test(url)) {
const channelDetails = source.getChannel(url);
url = URL_CHANNEL_BASE + channelDetails.id.value;
}
const match = url.match(REGEX_VIDEO_CHANNEL_URL);
if(!match || match.length != 3)
return {};
......@@ -1883,8 +1933,9 @@ class YTVideoSource extends VideoUrlRangeSource {
}
class YTABRVideoSource extends DashManifestRawSource {
constructor(obj, url, sourceObj, ustreamerConfig, bgData) {
constructor(itag, obj, url, sourceObj, ustreamerConfig, bgData, parentUrl, usedLogin) {
super(obj);
this.itag = itag;
this.url = url;
this.abrUrl = url;
this.ustreamerConfig = ustreamerConfig;
......@@ -1896,14 +1947,17 @@ class YTABRVideoSource extends DashManifestRawSource {
this.bgData = bgData;
this.visitorId = bgData.visitorData;
this.dataSyncId = bgData.dataSyncId
this.visitorDataType = bgData.visitorDataType;
this.parentUrl = parentUrl;
this.usedLogin = !!usedLogin;
}
generate() {
if(this.lastDash)
return this.lastDash;
log("Generating ABR Video Dash");
log("Generating ABR Video Dash for " + this.sourceObj.itag);
getMediaReusableVideoBuffers()?.freeAll();
let [dash, umpResp, fileHeader] = generateDash(this.sourceObj, this.ustreamerConfig, this.abrUrl, this.sourceObj.itag);
let [dash, umpResp, fileHeader] = generateDash(this, this.sourceObj, this.ustreamerConfig, this.abrUrl, this.sourceObj.itag);
this.initialHeader = fileHeader;
this.initialUMP = umpResp;
this.lastDash = dash;
......@@ -1916,14 +1970,15 @@ class YTABRVideoSource extends DashManifestRawSource {
return dash;
}
getRequestExecutor() {
return new YTABRExecutor(this.abrUrl, this.sourceObj, this.ustreamerConfig,
return new YTABRExecutor(this, this.abrUrl, this.sourceObj, this.ustreamerConfig,
this.initialHeader,
this.initialUMP, this.bgData);
}
}
class YTABRAudioSource extends DashManifestRawAudioSource {
constructor(obj, url, sourceObj, ustreamerConfig, bgData) {
constructor(itag, obj, url, sourceObj, ustreamerConfig, bgData, parentUrl, usedLogin) {
super(obj);
this.itag = itag;
this.url = url;
this.abrUrl = url;
this.ustreamerConfig = ustreamerConfig;
......@@ -1933,6 +1988,9 @@ class YTABRAudioSource extends DashManifestRawAudioSource {
this.bgData = bgData;
this.visitorId = bgData.visitorData;
this.dataSyncId = bgData.dataSyncId
this.visitorDataType = bgData.visitorDataType;
this.parentUrl = parentUrl;
this.usedLogin = !!usedLogin;
}
generate() {
......@@ -1940,7 +1998,7 @@ class YTABRAudioSource extends DashManifestRawAudioSource {
return this.lastDash;
log("Generating ABR Audio Dash");
getMediaReusableAudioBuffers()?.freeAll();
let [dash, umpResp, fileHeader] = generateDash(this.sourceObj, this.ustreamerConfig, this.abrUrl, this.sourceObj.itag);
let [dash, umpResp, fileHeader] = generateDash(this, this.sourceObj, this.ustreamerConfig, this.abrUrl, this.sourceObj.itag);
this.initialHeader = fileHeader;
this.initialUMP = umpResp;
this.lastDash = dash;
......@@ -1953,23 +2011,43 @@ class YTABRAudioSource extends DashManifestRawAudioSource {
return dash;
}
getRequestExecutor() {
return new YTABRExecutor(this.abrUrl, this.sourceObj, this.ustreamerConfig,
return new YTABRExecutor(this, this.abrUrl, this.sourceObj, this.ustreamerConfig,
this.initialHeader,
this.initialUMP, this.bgData);
}
}
function generateDash(sourceObj, ustreamerConfig, abrUrl, itag) {
function generateDash(parentSource, sourceObj, ustreamerConfig, abrUrl, itag) {
const now = (new Date()).getTime();
const lastAction = (new Date()).getTime() - (Math.random() * 5000);
const initialReq = getVideoPlaybackRequest(sourceObj, ustreamerConfig, 0, 0, 0, lastAction, now);
if(parentSource.pot)
log("Using POT for initial stream request");
if(abrUrl) {
if(abrUrl.indexOf("&cpn=") <= 0) {
abrUrl += "&cpn=" + randomString(16);
}
if(abrUrl.indexOf("&cver=") <= 0) {
abrUrl += "&cver=2.20250131.01.00";
}
if(abrUrl.indexOf("&rn=") <= 0) {
abrUrl += "&rn=1";
}
if(abrUrl.indexOf("&alr=") <= 0) {
abrUrl += "&alr=yes";
}
}
const initialReq = getVideoPlaybackRequest(sourceObj, ustreamerConfig, 0, 0, 0, lastAction, now, undefined, parentSource.pot);
const postData = initialReq.serializeBinary();
let initialResp = http.POST(abrUrl, postData, {
"Origin": "https://www.youtube.com",
"Accept": "*/*",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"//"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
}, false, true);
if(!initialResp.isOk)
if(!initialResp.isOk) {
throw new ScriptException("Failed initial stream request [ " + initialResp.code + "]");
}
const data = initialResp.body;
let byteArray = undefined;
......@@ -2030,25 +2108,39 @@ function generateDash(sourceObj, ustreamerConfig, abrUrl, itag) {
streams.push(stream);
}
const webmHeaderData = streams[0].data;
const webmHeader = new WEBMHeader(webmHeaderData,
sourceObj.mimeType.split(";")[0],
/codecs=\"(.+)\"/.exec(sourceObj.mimeType)[1],
sourceObj.width,
sourceObj.height);
const urlPrefix = (isVideo) ?
"https://grayjay.internal/video" :
"https://grayjay.internal/audio";
const dash = generateWEBMDash(webmHeader,
urlPrefix + "/internal/segment.webm?segIndex=$Number$",
urlPrefix + "/internal/init.webm");
return [dash, umpResp, webmHeader];
const headerData = streams[0].data;
let header = undefined;
if(sourceObj.mimeType.indexOf("webm") > 0) {
const webmHeader = new WEBMHeader(headerData, sourceObj.mimeType.split(";")[0], /codecs=\"(.+)\"/.exec(sourceObj.mimeType)[1], sourceObj.width, sourceObj.height);
const urlPrefix = (isVideo) ?
"https://grayjay.internal/video" :
"https://grayjay.internal/audio";
const dash = generateWEBMDash(webmHeader,
urlPrefix + "/internal/segment.webm?segIndex=$Number$",
urlPrefix + "/internal/init.webm");
return [dash, umpResp, webmHeader];
}
else if(sourceObj.mimeType.indexOf("mp4") > 0) {
const mp4Header = new MP4Header(headerData, sourceObj.mimeType.split(";")[0], /codecs=\"(.+)\"/.exec(sourceObj.mimeType)[1], sourceObj.width, sourceObj.height);
if(IS_TESTING)
console.log("Parsed MP4: ", mp4Header);
const urlPrefix = (isVideo) ?
"https://grayjay.internal/video" :
"https://grayjay.internal/audio";
const dash = generateWEBMDash(mp4Header,
urlPrefix + "/internal/segment.mp4?segIndex=$Number$",
urlPrefix + "/internal/init.mp4");
return [dash, umpResp, mp4Header];
}
else
throw new ScriptException("Unsupported mimetype: " + sourceObj.mimeType);
}
function generateWEBMDash(webm, templateUrl, initUrl) {
const duration = splitMS(webm.duration);
const durationFormatted = `PT${duration.hours}H${duration.minutes}M${duration.seconds}.${((duration.miliseconds + "").padStart(3, '0'))}S`;
const durationFormatted = `PT${duration.hours}H${duration.minutes}M${parseInt(duration.seconds)}.${((parseInt(duration.miliseconds) + "").padStart(3, '0'))}S`;
let repCounter = 1;
let mpd = `<?xml version="1.0" encoding="UTF-8"?>\n`;
......@@ -2076,12 +2168,13 @@ function generateWEBMDash(webm, templateUrl, initUrl) {
xmlTag("S", {t: cue, d: (webm.cues.length > i + 1) ? webm.cues[i + 1] - cue : webm.durationCueTimescale - cue}, undefined, indent + " ")
).join("")
,indent + " ")
,indent + " ")
,indent + " ")
+ " \n"//TEMPORARY FIX, FIX MATCH REPLACEMENT
,indent + " ")
, indent + " ")
, indent + " ")
, "");
log(mpd);
return mpd;
}
function splitMS(ms) {
......@@ -2208,8 +2301,10 @@ const useReusableBuffers = false;
let executorCounter = 0;
let _executorsVideo = [];
let _executorsAudio = [];
let _recoveryCache = {};
class YTABRExecutor {
constructor(url, source, ustreamerConfig, header, initialUmp, bgData) {
constructor(parentSource, url, source, ustreamerConfig, header, initialUmp, bgData) {
this.parentSource = parentSource;
this.executorId = executorCounter++;
this.source = source;
this.itag = source.itag;
......@@ -2222,14 +2317,23 @@ class YTABRExecutor {
this.requestStarted = (new Date()).getTime();
this.lastAction = (new Date()).getTime() - (Math.random() * 1000 * 5);
this.segmentOffsets = undefined;
this.pot = undefined;
this.lastWorkingPot = undefined;
this.bgData = bgData;
this.level = 0;
this.childExecutor = undefined;
if(bgData)
if(bgData) {
if(!bgData.visitorId && !bgData.dataSyncId) {
log("Botguard no visitorId or dataSyncId found, not using botguard!");
}
tryGetBotguard((bg)=>{
bg.getTokenOrCreate(bgData.visitorData, bgData.dataSyncId, (pot)=>{
console.log("Botguard Token to use:", pot);
this.pot = pot;
});
}, bgData.visitorDataType);
});
}
log("UMP New executor: " + source.name + " - " + source.mimeType + " (segments: " + header?.cues?.length + ")");
log("UMP Cues: " + header?.cues?.join(", "));
......@@ -2279,6 +2383,8 @@ class YTABRExecutor {
log("UMP Cues: " + this.header.cues.join(", "));
throw new ScriptException("Zero time for non-zero segment?");
}
if(this.header.cueTimeScale)
return parseInt((time / this.header.cueTimeScale) * 1000);
return time;
}
else
......@@ -2317,11 +2423,16 @@ class YTABRExecutor {
const reusable = this.reusableBuffer;
for(let key of Object.keys(this.segments)) {
reusable?.free(this.segments[key].data);
const buffer = this.segments[key]?.data?.buffer;
if(buffer && !buffer.detached)
buffer?.transfer();
delete this.segments[key];
}
}
cleanup() {
if(this.childExecutor)
return this.childExecutor.cleanup();
log("UMP: Cleaning up!");
this.initialUmp = undefined;
this.header = undefined;
......@@ -2337,9 +2448,46 @@ class YTABRExecutor {
log("Remaining audio executors: " + _executorsAudio.length);
}
this.freeAllSegments();
console.clear(); //Temp fix for memory leaking
}
recreateExecutor(){
const parentUrl = this.parentSource.parentUrl;
console.warn("Re-fetching [" + parentUrl + "] for executor");
if(!parentUrl)
throw new ScriptException("Failed to recreate object");
const video = source.getContentDetails(parentUrl, this.parentSource.usedLogin, true, true);
let newSource = undefined;
if(this.source.mimeType.startsWith("video/"))
newSource = video.video.videoSources.find(x=>x.itag == this.itag);
else
newSource = video.video.audioSources.find(x=>x.itag == this.itag);
console.warn("Re-fetched source", newSource);
if(!newSource)
throw new ScriptException("Could not re-find itag " + this.itag);
//TODO: Cache video
console.warn("Re-generate UMP Dash");
newSource.generate();
const newExecutor = newSource.getRequestExecutor();
if(!newExecutor)
throw new ScriptException("No executor found in re-fetched source for " + this.itag);
this.cleanup();
this.childExecutor = newExecutor;
//this.abrUrl = newSource.abrUrl;
bridge.toast("UMP [" + this.type + "] Recovered");
}
executeRequest(url, headers, retryCount, overrideSegment) {
console.clear();
if(this.childExecutor)
return this.childExecutor.executeRequest(url, headers, retryCount, overrideSegment);
if(!retryCount)
retryCount = 0;
log("UMP: " + url + "");
......@@ -2365,11 +2513,11 @@ class YTABRExecutor {
log("UMP [" + this.type + "] Cached segment " + segment + " was undefined, refetching");
}
log("UMP [" + this.type + "] requesting segment: " + segment + ", time: " + time + ", itag: " + this.itag);
const pot = this.pot;
log("UMP [" + this.type + "] requesting segment: " + segment + ", time: " + time + ", itag: " + this.itag + (pot ? (", pot:" + pot.substring(0, 5) + "..") : ""));
if(overrideSegment)
log("UMP [" + this.type + "] requesting with overrided segment: " + overrideSegment)
const now = (new Date()).getTime();
const pot = this.pot;
const initialReq = getVideoPlaybackRequest(this.source, this.ustreamerConfig, time, (overrideSegment) ? overrideSegment : segment, this.lastRequest, this.lastAction, now, this.playbackCookie, pot);
const postData = initialReq.serializeBinary();
const initialResp = http.POST(this.abrUrl, postData, {
......@@ -2412,10 +2560,41 @@ class YTABRExecutor {
this.lastRequest = (new Date()).getTime();
const stream = streamsArr[0];
if(!stream)
if(!stream) {
if(umpResp.redirectUrl) {
log("UMP Responded with redirect Url: " + umpResp.redirectUrl);
}
log("UMP no stream, try recovery: \n" +
" - Had POT: " + !!pot + "\n" +
" - POT Worked: " + (this.lastWorkingPot == pot) + "\n" +
" - Has Redirect: " + !!umpResp.redirectUrl);
if(pot && this.lastWorkingPot == pot && !!umpResp.redirectUrl) {
this.lastWorkingPot = undefined;
log("UMP [" + this.type + "] No stream despite POT working before, swapping url..\nBEFORE: " + this.abrUrl + "\n" + umpResp.redirectUrl);
console.warn("UMP [" + this.type + "] broke, attempting recovery")
this.abrUrl = umpResp.redirectUrl;
//TODO: Implement proper recovery instead of this hackfix.
if(!!_settings.useAggressiveUMPRecovery) {
const botGuard = getExistingBotguard();
if(!botGuard) {
log("Botguard generator didn't exist? Letting it throw");
}
else {
console.warn("Regenerating executor due to missing streams");
if(!!_settings.notify_ump_recovery)
bridge.toast("UMP [" + this.type + "] no streams, attempting recovery (ip change?)");
this.recreateExecutor();
return this.executeRequest(url, headers, retryCount, overrideSegment);
}
}
}
throw new ScriptException("No streams for requesting segment " + segment + ((overrideSegment && overrideSegment > 0) ? (", override: " + overrideSegment) : ""));
}
const expectedSegment = parseInt(segment) + parseInt(this.getOffset(stream.segmentIndex));
log("Expected segment " + expectedSegment + " got " + stream.segmentIndex);
if(stream && stream.segmentIndex != expectedSegment) {
......@@ -2470,6 +2649,10 @@ class YTABRExecutor {
if(!stream || !stream.data)
throw new ScriptException("NO STREAMDATA FOUND (" + Object.keys(umpResp.streams).join(", ") + "): " + !!umpResp.streams[0]?.data);
if(pot && this.lastWorkingPot != pot) {
this.lastWorkingPot = pot;
}
log("UMP [" + this.type + "]: segment " + segment + " - " + stream.data?.length);
return stream.data;
}
......@@ -2481,7 +2664,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn
const clientInfo = new pb.VideoPlaybackRequest_pb.ClientInfo();
clientInfo.setClientname(1);
clientInfo.setClientversion("2.20250107.01.00");
clientInfo.setClientversion("2.20250131.01.00");
clientInfo.setOsname("Windows");
clientInfo.setOsversion("10.0");
......@@ -2494,12 +2677,12 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn
info.setVideoheight2maybe(source.height);
info.setSelectedqualityheight(source.height);
}
info.setG7(8613683);
info.setCurrentvideopositionms(playerPosMs);
info.setG7(104857); //x
info.setCurrentvideopositionms(playerPosMs); //x
if(lastRequest > 0)
info.setTimesincelastrequestms((new Date().getTime() - lastRequest));
info.setTimesincelastactionms(Math.floor((new Date()).getTime() - lastAction));
info.setDynamicrangecompression(true);
info.setTimesincelastrequestms((new Date().getTime() - lastRequest)); //x
info.setTimesincelastactionms(Math.floor((new Date()).getTime() - lastAction)); //x
info.setDynamicrangecompression(true); //x
info.setLatencymsmaybe(Math.floor(Math.random() * 90 + 7));
info.setLastmanualdirection(0);
info.setTimesincelastmanualformatselectionms(requestStarted);
......@@ -2523,6 +2706,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn
if(source.xtags)
format.setXtags(source.xtags);
if(segmentIndex > 0) {
const bufferedStream = new pb.VideoPlaybackRequest_pb.BufferedStreamInfo()
bufferedStream.setFormatid(format);
......@@ -2530,7 +2714,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn
bufferedStream.setBufferedsegmentstartindex(1);
bufferedStream.setBufferedsegmentendindex(segmentIndex - 1);
bufferedStream.setBufferedstarttimems(0);
//bufferedStream.setBuffereddurationms(playerPosMs);
bufferedStream.setBuffereddurationms(playerPosMs);
vidReq.setBufferedstreamsList[bufferedStream];
vidReq.setDesiredstreamsList([format]);
}
......@@ -3263,7 +3447,7 @@ function requestClientConfig(useMobile = false, useAuth = false) {
return getClientConfig(resp.body);
}
function requestIOSStreamingData(videoId, batch) {
function requestIOSStreamingData(videoId, batch, visitorData, useLogin) {
const body = {
videoId: videoId,
cpn: "" + randomString(16),
......@@ -3280,12 +3464,23 @@ function requestIOSStreamingData(videoId, batch) {
"osVersion": IOS_OS_VERSION_DETAILED,//"15.6.0.19G71",^M
"hl": langDisplay,
"gl": langRegion,
"utcOffsetMinutes": 0
},
user: {
"lockedSafetyMode": false
}
}
};
const visitorToken = visitorData?.visitorData ?? visitorData?.dataSyncId;
if(visitorToken && !useLogin) {
body.context.client.visitorData = visitorToken;
}
else if(visitorData?.visitorDataLogin && useLogin){
body.context.client.visitorData = visitorData?.visitorDataLogin;
}
else if(visitorData?.dataSyncId && useLogin) {
body.context.client.datasyncId = visitorData?.dataSyncId;
}
const headers = {
"Content-Type": "application/json",
"User-Agent": USER_AGENT_IOS,
......@@ -3301,11 +3496,11 @@ function requestIOSStreamingData(videoId, batch) {
"&id=" + videoId
if(batch) {
batch.POST(url, JSON.stringify(body), headers, false);
batch.POST(url, JSON.stringify(body), headers, !!useLogin);
return null;
}
else {
const resp = http.POST(url, JSON.stringify(body), headers, false);
const resp = http.POST(url, JSON.stringify(body), headers, !!useLogin);
return resp;
}
}
......@@ -3728,7 +3923,7 @@ function extractPage_Tabs(initialData, contextData) {
//#region Layout Extractors
function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextData, jsUrl, useLogin, useAbr, clientConfig) {
function extractVideoPage_VideoDetails(parentUrl, initialData, initialPlayerData, contextData, jsUrl, useLogin, useAbr, clientConfig, usedLogin) {
const contents = initialData.contents;
const contentsContainer = contents.twoColumnWatchNextResults?.results?.results ??
null;
......@@ -3770,7 +3965,7 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa
video:
((!useAbr) ?
extractAdaptiveFormats_VideoDescriptor(initialPlayerData?.streamingData?.adaptiveFormats, jsUrl, contextData, "") :
extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig)
extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig, parentUrl, usedLogin)
)
?? new VideoSourceDescriptor([]),
subtitles: initialPlayerData
......@@ -3943,7 +4138,32 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa
return result;
}
function extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig) {
function getBGDataFromClientConfig(clientConfig, usedLogin) {
let visitorDataType = "Unknown";
if(usedLogin)
visitorDataType = "DataSyncID";
else if(clientConfig?.EOM_VISITOR_DATA)
visitorDataType = "EOM";
else if(clientConfig?.VISITOR_DATA)
visitorDataType = "VisitorData";
else
visitorDataType = "Unknown";
const visitorData = usedLogin ? null : (clientConfig?.EOM_VISITOR_DATA ?? clientConfig?.VISITOR_DATA);
const visitorDataLogin = usedLogin ? (clientConfig?.EOM_VISITOR_DATA ?? clientConfig?.VISITOR_DATA) : null;
console.log("VisitorData: ", visitorData);
log("VisitorDataType: " + visitorDataType);
return {
visitorData: visitorData?.replaceAll("%3D", "="),
visitorDataLogin: visitorDataLogin?.replaceAll("%3D", "="),
dataSyncId: clientConfig?.DATASYNC_ID,
visitorDataType: visitorDataType
}
}
function extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig, parentUrl, usedLogin) {
const abrStreamingUrl = (initialPlayerData.streamingData.serverAbrStreamingUrl) ?
decryptUrlN(initialPlayerData.streamingData.serverAbrStreamingUrl, jsUrl, false) : undefined;
......@@ -3952,43 +4172,50 @@ function extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clien
return new UnMuxVideoSourceDescriptor(
(initialPlayerData.streamingData.adaptiveFormats
.filter(x => x.mimeType.startsWith("video/webm"))
.filter(x => x.mimeType.startsWith("video/webm") || x.mimeType.startsWith("video/mp4"))
.map(y => {
const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1);
const container = y.mimeType.substring(0, y.mimeType.indexOf(';'));
if (codecs.startsWith("av01"))
const isAV1 = codecs.startsWith("av01");
if (!_settings.allow_av1 && isAV1)
return null; //AV01 is unsupported.
const duration = parseInt(parseInt(y.approxDurationMs) / 1000) ?? 0;
if (isNaN(duration))
return null;
console.log("VisitorData: ", clientConfig?.EOM_VISITOR_DATA);
return new YTABRVideoSource({
name: "UMP " + y.height + "p" + (y.fps ? y.fps : "") + " " + container,
if(isAV1)
log("FOUND AV1: " + "UMP " + y.height + "p" + (y.fps ? y.fps : "") + " " + container + ((isAV1) ? " [AV1]" : ""));
const result = new YTABRVideoSource(y.itag, {
name: "UMP " + y.height + "p" + (y.fps ? y.fps : "") + " " + container + ((isAV1) ? " [AV1]" : ""),
url: abrStreamingUrl,
width: y.width,
height: y.height,
duration: (!isNaN(duration)) ? duration : 0,
container: y.mimeType.substring(0, y.mimeType.indexOf(';')),
codec: codecs,
bitrate: y.bitrate,
bitrate: y.bitrate
}, abrStreamingUrl, y, initialPlayerData.playerConfig.mediaCommonConfig.mediaUstreamerRequestConfig.videoPlaybackUstreamerConfig,
{ visitorData: clientConfig?.EOM_VISITOR_DATA?.replaceAll("%3D", "="), dataSyncId: clientConfig?.DATASYNC_ID});
getBGDataFromClientConfig(clientConfig, usedLogin), parentUrl, usedLogin);
result.priority = isAV1;
return result;
})).filter(x => x != null),
//Audio
(initialPlayerData.streamingData.adaptiveFormats
.filter(x => x.mimeType.startsWith("audio/webm"))
.filter(x => x.mimeType.startsWith("audio/webm") || x.mimeType.startsWith("audio/mp4"))
.map(y => {
const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1);
const container = y.mimeType.substring(0, y.mimeType.indexOf(';'));
if (codecs.startsWith("av01"))
const isAV1 = codecs.startsWith("av01");
if (!_settings.allow_av1 && isAV1)
return null; //AV01 is unsupported.
const duration = parseInt(parseInt(y.approxDurationMs) / 1000) ?? 0;
if (isNaN(duration))
return null;
return new YTABRAudioSource({
name: "UMP " + (y.audioTrack?.displayName ? y.audioTrack.displayName : codecs),
return new YTABRAudioSource(y.itag, {
name: "UMP " + (y.audioTrack?.displayName ? y.audioTrack.displayName : codecs) + ((isAV1) ? " [AV1]" : ""),
url: abrStreamingUrl,
width: y.width,
height: y.height,
......@@ -3999,7 +4226,7 @@ function extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clien
audioChannels: y.audioChannels,
language: ytLangIdToLanguage(y.audioTrack?.id)
}, abrStreamingUrl, y, initialPlayerData.playerConfig.mediaCommonConfig.mediaUstreamerRequestConfig.videoPlaybackUstreamerConfig,
{ visitorData: clientConfig?.EOM_VISITOR_DATA?.replaceAll("%3D", "="), dataSyncId: clientConfig?.DATASYNC_ID});
getBGDataFromClientConfig(clientConfig, usedLogin), parentUrl, usedLogin);
})).filter(x => x != null)
);
}
......@@ -5559,6 +5786,10 @@ source.decryptUrlTestN = function(n) {
return decryptUrlN(url, true);
}
source.decryptUrlN = function(url, jsUrl) {
prepareCipher(jsUrl);
return decryptUrlN(url, jsUrl, true);
}
function decryptUrl(encrypted, jsUrl, doLogging) {
if(!encrypted) return null;
......@@ -5740,7 +5971,7 @@ function getNDecryptorFunctionCode(code, jsUrl) {
throw new ScriptException("Failed to find n decryptor (code)\n" + jsUrl);
}
const regex = new RegExp(/typeof ([a-zA-Z0-9]+)/gs);
const regex = new RegExp(/typeof ([a-zA-Z0-9$_]+)/gs);
const typeChecks = [];
let prefix = "";
let typeCheck = undefined;
......@@ -5900,6 +6131,11 @@ class UMPResponse {
if(stream22)
stream22.completed = true;
break;
case 29: //Unknown
const opCode29 = pb.Opcode29_pb.Opcode29.deserializeBinary(segment);
console.log("");
break;
case 35://Opcode35: Playbackcookie
const opCode35 = pb.Opcode35_pb.Opcode35.deserializeBinary(segment);
this.playbackCookie = opCode35.getPlaybackcookie();
......@@ -5907,6 +6143,13 @@ class UMPResponse {
case 43://Message
const opCode43 = pb.Opcode43_pb.Opcode43.deserializeBinary(segment);
this.redirectUrl = opCode43?.getRedirecturl();
log("Redirect url found: " + this.redirectUrl);
case 44: //Unknown
const opCode44 = pb.Opcode44_pb.Opcode44.deserializeBinary(segment);
console.error("UMP Error", opCode44.getBda());
log("Error:" + opCode44.getBda());
console.log("");
break;
}
}
else {
......@@ -5916,7 +6159,7 @@ class UMPResponse {
}
}
if(this.streamCount == 0) {
log("UMP: No streams found?");
log("UMP: No streams found? (Opcodes: " + this.opcodes.map(x=>`${x.opcode}:${x.length}`).join(", ") + ")");
if(bytes.length < 200) {
log("UMP Resp: " + bytes.join(" ") + "\nOpcodes: " + this.opcodes.map(x=>`${x.opcode}:${x.length}`).join(", "));
return undefined;
......@@ -6038,17 +6281,6 @@ function binaryReadFloat(bytes, pointer, size) {
//#endregion
//#region MP4
class MP4Header {
constructor(bytes) {
}
}
//#endregion
let pb = {};
source.testProtobuf = function() {
let test2 = new pb.VideoPlaybackRequest_pb.VideoPlaybackRequestInfo();
......@@ -6084,24 +6316,110 @@ source.testing = function(url) {
return generated;
}
source.testUMP = async function(url, startSegment, endSegment, loops = 2){
source.testUMPRecovery = async function(){
const url = ""
USE_ABR_VIDEOS = true;
const item = this.getContentDetails(url);
console.log(item);
const video = item.video.videoSources.find(x=>x.name.startsWith("UMP") && x.container == "video/webm" && x.height == 480);
const video = item.video.videoSources.find(x=>x.name.startsWith("UMP") && x.container == "video/mp4" && x.height == 720);
const generated = video.generate();
const executor = video.getRequestExecutor();
for(let i = 0; i < loops; i++) {
console.log("Loop: " + i);
let failed = false;
for(let i = 12; i < 18; i++) {
for(let x = 0; x < 3; x++) {
try {
executor.executeRequest("https://grayjay.app/internal/video?segIndex=" + i, {});
break;
}
catch(ex) {
console.error("FAILED REQ (" + i + "): " + ex);
await delay(1000);
}
if(x == 2)
failed = true;
}
if(failed)
break;
await delay(2000);
if(i == 15)
alert("Change network and press ok");
}
return;
};
source.testUMP = async function(url, startSegment, endSegment, itag, isAudio){
USE_ABR_VIDEOS = true;
const item = this.getContentDetails(url);
console.log(item);
let video = (!isAudio) ?
item.video.videoSources.find(x=>x.name.startsWith("UMP") && x.itag == itag) :
item.video.audioSources.find(x=>x.name.startsWith("UMP") && x.itag == itag);
if(!video)
video = item.video.videoSources.find(x=>x.name.startsWith("UMP") && x.container == "video/mp4" && x.height == 720);
setTimeout(async ()=>{
const generated = video.generate();
console.log("Generated:", generated);
const executor = video.getRequestExecutor();
for(let i = startSegment; i < endSegment; i++) {
executor.executeRequest("https://grayjay.app/internal/video?segIndex=" + i, {});
await delay(2000);
if(endSegment && endSegment > 0) {
for(let i = startSegment; i < endSegment; i++) {
const resp = executor.executeRequest("https://grayjay.app/internal/video?segIndex=" + i, {});
resp.buffer.transfer();
await delay(2000);
}
}
}
executor.cleanup();
}, 1000);
return;
};
source.testIOS = async function(url, itag, isAudio){
const item = this.getContentDetails(url);
console.log(item);
let video = (!isAudio) ?
item.video.videoSources.find(x=>x.name.startsWith("IOS") && x.itag == itag) :
item.video.audioSources.find(x=>x.name.startsWith("IOS") && x.itag == itag);
if(!video)
video = item.video.videoSources.find(x=>x.name.startsWith("IOS") && x.container == "video/mp4" && x.height == 720);
setTimeout(async ()=>{
const modifier = video.getRequestModifier();
console.log(modifier);
const modified = modifier.modifyRequest(video.url, {
"range": "bytes=0-10240"
});
const resp1 = http.GET(modified.url, modified.headers, false);
console.log(resp1);
setTimeout(()=>{
const modified2 = modifier.modifyRequest(video.url, {
"accept-ranges": "bytes",
"range": "bytes=55103125-55603125"
});
const resp2 = http.GET(modified2.url, modified2.headers, false);
console.log(resp2);
if(resp1.isOk)
console.log("REQUEST START: PASS")
else
console.warn("REQUEST START: FAIL [" + resp1.code + "]");
if(resp2.isOk)
console.log("REQUEST MIDDLE: PASS");
else
console.warn("REQUEST MIDDLE: FAIL [" + resp2.code + "]");
});
}, 500);
return;
};
const delay = (delayInms) => {
......@@ -6110,6 +6428,132 @@ const delay = (delayInms) => {
console.log("LOADED");
//#region MP4
class MP4Header {
constructor(bytes, mimeType, codec, width, height) {
this.timescale = 0;
this.duration = 0;
this.cues = [];
this.mimeType = mimeType;
this.codec = codec;
this.width = width;
this.height = height;
this.samplingFrequency = 48000;
let pointer = {index: 0};
function readBoxHeader(pointer) {
const size = binaryReadUInt(bytes, pointer, 4) - 8;
const type = String.fromCharCode(...binaryReadBytes(bytes, pointer, 4));
return [type, size];
}
let mp4Duration = -1;
let mp4DurationInCueTimescale = -1;
let mp4DurationTimescale = -1;
let mp4CueTimescale = -1;
let foundTypes = [];
while(pointer.index < bytes.length) {
const [type, size] = readBoxHeader(pointer);
const startOffset = pointer.index;
foundTypes.push(type + ": " + size);
switch(type) {
case "moov":
while(pointer.index - startOffset < size && pointer.index < bytes.length) {
const [moovType, moovSize] = readBoxHeader(pointer);
switch(moovType) {
case "mvhd":
const mvhdStartOffset = pointer.index;
const version = binaryReadByte(bytes, pointer);
const flags = binaryReadBytes(bytes, pointer, 3);
if(version == 1) {
const creationTime = binaryReadUInt(bytes, pointer, 8);
const modifyTime = binaryReadUInt(bytes, pointer, 8);
}
else {
const creationTime = binaryReadUInt(bytes, pointer, 4);
const modifyTime = binaryReadUInt(bytes, pointer, 4);
}
mp4DurationTimescale = binaryReadUInt(bytes, pointer, 4);
if(version == 1)
mp4Duration = binaryReadUInt(bytes, pointer, 8);
else
mp4Duration = binaryReadUInt(bytes, pointer, 4);
pointer.index = mvhdStartOffset + moovSize;
break;
default:
pointer.index += moovSize;
break;
}
}
if(pointer.index > startOffset + size) {
throw new ScriptException("Invalid amount of bytes read from moov section.");
}
break;
case "sidx":
this.indexRangeStart = startOffset;
this.indexRangeEnd = startOffset + size;
const version = binaryReadByte(bytes, pointer);
const flags = binaryReadBytes(bytes, pointer, 3);
const referenceID = binaryReadUInt(bytes, pointer, 4);
mp4CueTimescale = binaryReadUInt(bytes, pointer, 4);
mp4DurationInCueTimescale = parseInt((mp4Duration / mp4DurationTimescale) * mp4CueTimescale);
let earliestPresentationTime = -1;
if(version == 0) {
earliestPresentationTime = binaryReadUInt(bytes, pointer, 4);
let firstOffset = binaryReadUInt(bytes, pointer, 4);
}
else {
earliestPresentationTime = binaryReadUInt(bytes, pointer, 8);
let firstOffset = binaryReadUInt(bytes, pointer, 8);
}
binaryReadUInt(bytes, pointer, 2);
const referenceCount = binaryReadUInt(bytes, pointer, 2);
let currentPresentationTime = earliestPresentationTime;
this.cues.push(currentPresentationTime);
for(let i = 0; i < referenceCount - 1; i++) {
const referenceSize = binaryReadUInt(bytes, pointer, 4);
const segmentDuration = binaryReadUInt(bytes, pointer, 4);
const deltaTimeStartsWith = binaryReadUInt(bytes, pointer, 4);
currentPresentationTime += segmentDuration;
this.cues.push(currentPresentationTime);
}
binaryReadUInt(bytes, pointer, 4); //referenceSize
const lastSegmentDuration = binaryReadUInt(bytes, pointer, 4);
binaryReadUInt(bytes, pointer, 4);
if(((this.cues[this.cues.length - 1] + lastSegmentDuration) - mp4DurationInCueTimescale) > 2)
throw new ScriptException("Cue points not lining up.");
if(pointer.index != startOffset + size)
throw new ScriptException("Invalid amount of bytes read from sidx section.");
break;
default:
pointer.index += size;
break;
}
}
console.log("MP4 Segments:", foundTypes);
this.durationSeconds = mp4Duration / mp4DurationTimescale;
this.durationCueTimescale = parseInt((mp4Duration / mp4DurationTimescale) * mp4CueTimescale);
this.cueTimeScale = mp4CueTimescale;
this.timescale = mp4CueTimescale * 1000;
this.duration = parseInt((mp4Duration / mp4DurationTimescale) * 1000);
}
}
//#endregion
//#region WEBM
class WEBMHeader {
......@@ -6292,6 +6736,9 @@ function tryGetBotguard(cb) {
cb(botguard);
}, 100);
}
function getExistingBotguard(){
return existingBotguard;
}
//#region BotGuard
class BotGuardGeneratorInput {
......@@ -6321,6 +6768,10 @@ class BotGuardGenerator {
}
initialize() {
this.mint = undefined;
this.mintConstructor = undefined;
this.minterFailure = undefined;
this.ready = false;
console.log("VM Initializing");
const requestKey = this.requestKey;
......@@ -6449,6 +6900,14 @@ class BotGuardGenerator {
});
}
}
recreateMinter() {
if (!this.mintConstructor)
throw "No Mint Constructor";
if (!this.snapshotResult)
throw "No Snapshot Result";
const minter = this.constructMinter();
return minter;
}
constructMinter() {
if (!this.mintConstructor)
throw "No Mint Constructor";
......@@ -6483,33 +6942,88 @@ class BotGuardGenerator {
return this.mint;
}
getTokenOrCreate(visitorId, dataSync, cb) {
getTokenOrCreate(visitorId, dataSyncId, cb, type) {
if(!visitorId && !dataSyncId)
throw new ScriptException("No visitor or datasync Id provided for botguard");
let idToUse = visitorId ?? dataSyncId;
//TODO: Handle datasync scenario
const existing = this.generatedTokens[visitorId];
const existing = this.generatedTokens[idToUse];
if(!existing) {
log("No existing botguard token, generating new");
this.generateBase64(visitorId, dataSync, (token)=>{
this.generateBase64(visitorId, dataSyncId, (token)=>{
cb(token);
});
}, type);
}
else //TODO: check expiry?
cb(existing.tokenBase64);
}
generateBase64(visitorId, dataSync, cb) {
this.generate(visitorId, dataSync, (result) => {
generateBase64(visitorId, dataSyncId, cb, type) {
this.generate(visitorId, dataSyncId, (result) => {
const originId = visitorId;
let poToken = btoa(String.fromCharCode(...result))
.replace(/\+/g, '-')
.replace(/\//g, '_');
cb(poToken);
});
}, type);
}
generate(visitorId, dataSync, cb) {
generate(visitorId, dataSyncId, cb, type) {
if(!visitorId && !dataSyncId)
throw new ScriptException("No visitor or datasync Id provided for botguard");
const idToUse = visitorId ?? dataSyncId;
if(idToUse == dataSyncId)
log("BOTGUARD USING DATASYNCID");
this.getMinter((minter, expire) => {
console.log("Minting visitor: ", visitorId);
const poToken = minter(new TextEncoder().encode(visitorId));
try {
console.log("Minting visitor: " + idToUse);
const poToken = minter(new TextEncoder().encode(idToUse));
const poTokenBase64 = btoa(String.fromCharCode(...poToken))
.replace(/\+/g, '-')
.replace(/\//g, '_');
//TODO: Handle dataSync
const newPoToken = {
token: poToken,
tokenBase64: poTokenBase64,
expires: expire
};
this.generatedTokens[idToUse] = newPoToken;
log("New PO Token: " + newPoToken.tokenBase64);
if(_settings?.notify_bg)
bridge.toast("New Botguard Token: " + (type ? "(" + type + ") " : "") + newPoToken?.tokenBase64?.substring(0, 10) + "...");
cb(poToken);
}
catch(ex) {
log("Minting failed due to: " + ex);
}
});
}
generateBase64Sync(visitorId, dataSyncId, cb, type) {
const result = this.generateSync(visitorId, dataSyncId, type);
if(!result)
return undefined;
const originId = visitorId;
let poToken = btoa(String.fromCharCode(...result))
.replace(/\+/g, '-')
.replace(/\//g, '_');
return poToken;
}
generateSync(visitorId, dataSyncId, type){
if(!this.mint)
return undefined;
try {
const minter = this.mint;
if(!visitorId && !dataSyncId)
throw new ScriptException("No visitor or datasync Id provided for botguard");
let idToUse = visitorId ?? dataSyncId;
console.log("Minting visitor: " + idToUse);
const poToken = minter(new TextEncoder().encode(idToUse));
const poTokenBase64 = btoa(String.fromCharCode(...poToken))
.replace(/\+/g, '-')
.replace(/\//g, '_');
......@@ -6518,14 +7032,17 @@ class BotGuardGenerator {
const newPoToken = {
token: poToken,
tokenBase64: poTokenBase64,
expires: expire
expires: this.mintExpire
};
this.generatedTokens[visitorId] = newPoToken;
this.generatedTokens[idToUse] = newPoToken;
log("New PO Token: " + newPoToken.tokenBase64);
if(_settings?.notify_bg)
bridge.toast("New Botguard Token: " + newPoToken?.tokenBase64?.substring(0, 10) + "...");
cb(poToken);
});
bridge.toast("New Botguard Token: " + (type ? "(" + type + ") " : "") + newPoToken?.tokenBase64?.substring(0, 10) + "...");
return poToken;
}
catch(ex) {
log("Minting failed due to: " + ex);
}
}
}
......@@ -8,7 +8,7 @@
"repositoryUrl": "https://futo.org",
"scriptUrl": "./YoutubeScript.js",
"version": 220,
"version": 231,
"iconUrl": "./youtube.png",
"id": "35ae969a-a7db-11ed-afa1-0242ac120003",
......@@ -71,6 +71,14 @@
"type": "Boolean",
"default": "false"
},
{
"variable": "channelRssOnly",
"name": "Only Use Channel RSS Feeds (Inferior)",
"description": "Exclusively use channel RSS feeds for channel content, may result in inferior results, and only recent videos. But may be faster and reduce rate limiting.",
"type": "Boolean",
"default": "false",
"warningDialog": "Using RSS feeds will have inferior results, and may add shorts in the channel videos and subscriptions.\n\nOld videos may also be unavailable."
},
{
"variable": "allowAgeRestricted",
"name": "Allow Age Restricted",
......@@ -92,6 +100,13 @@
"type": "Boolean",
"default": "false"
},
{
"variable": "useAggressiveUMPRecovery",
"name": "Use Aggressive UMP Recovery",
"description": "This feature allows UMP to refetch the entire page to recover from ip changes and such.",
"type": "Boolean",
"default": "true"
},
{
"variable": "showVerboseToasts",
"name": "Show Verbose Messages",
......@@ -203,6 +218,14 @@
"description": "These are settings not intended for most users, but may help development or power users.",
"type": "Header"
},
{
"variable": "allow_av1",
"name": "Allow AV1",
"description": "Adds AV1 option when available, MAY NOT BE SUPPORTED YET!",
"type": "Boolean",
"default": "false",
"warningDialog": "AV1 support might not work yet, this allows you to return the stream even if its not supported (for testing)"
},
{
"variable": "notify_cipher",
"name": "Show Cipher every Video",
......@@ -216,6 +239,13 @@
"description": "Shows a toast with the botguard token used changed",
"type": "Boolean",
"default": "false"
},
{
"variable": "notify_ump_recovery",
"name": "Show every time UMP disconnects",
"description": "Shows a toast whenever UMP goes into a reconnection mode",
"type": "Boolean",
"default": "false"
}
],
......