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 @@ ...@@ -8,7 +8,7 @@
"repositoryUrl": "https://futo.org", "repositoryUrl": "https://futo.org",
"scriptUrl": "./YoutubeScript.js", "scriptUrl": "./YoutubeScript.js",
"version": 220, "version": 231,
"iconUrl": "./youtube.png", "iconUrl": "./youtube.png",
"id": "35ae969a-a7db-11ed-afa1-0242ac120002", "id": "35ae969a-a7db-11ed-afa1-0242ac120002",
...@@ -71,6 +71,14 @@ ...@@ -71,6 +71,14 @@
"type": "Boolean", "type": "Boolean",
"default": "false" "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", "variable": "allowAgeRestricted",
"name": "Allow Age Restricted", "name": "Allow Age Restricted",
...@@ -92,6 +100,13 @@ ...@@ -92,6 +100,13 @@
"type": "Boolean", "type": "Boolean",
"default": "false" "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", "variable": "showVerboseToasts",
"name": "Show Verbose Messages", "name": "Show Verbose Messages",
...@@ -203,6 +218,14 @@ ...@@ -203,6 +218,14 @@
"description": "These are settings not intended for most users, but may help development or power users.", "description": "These are settings not intended for most users, but may help development or power users.",
"type": "Header" "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", "variable": "notify_cipher",
"name": "Show Cipher every Video", "name": "Show Cipher every Video",
...@@ -216,6 +239,13 @@ ...@@ -216,6 +239,13 @@
"description": "Shows a toast with the botguard token used changed", "description": "Shows a toast with the botguard token used changed",
"type": "Boolean", "type": "Boolean",
"default": "false" "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 @@ ...@@ -241,5 +271,14 @@
}, },
"supportedClaimTypes": [2], "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) { ...@@ -131,6 +131,11 @@ function getClientContext(isAuth = false) {
return (isAuth) ? _clientContextAuth : _clientContext; return (isAuth) ? _clientContextAuth : _clientContext;
} }
var _setMetadata = false;
source.enableMetadata = function() {
_setMetadata = true;
}
//#region Source Methods //#region Source Methods
source.setSettings = function(settings) { source.setSettings = function(settings) {
_settings = settings; _settings = settings;
...@@ -376,10 +381,12 @@ if(false && (bridge.buildSpecVersion ?? 1) > 1) { ...@@ -376,10 +381,12 @@ if(false && (bridge.buildSpecVersion ?? 1) > 1) {
//TODO: Implement more compact version using new api batch spec //TODO: Implement more compact version using new api batch spec
} }
else { else {
source.getContentDetails = (url, useAuth, simplify) => { source.getContentDetails = (url, useAuth, simplify, forceUmp) => {
useAuth = !!_settings?.authDetails || !!useAuth; useAuth = !!_settings?.authDetails || !!useAuth;
console.clear(); //Temp fix for memory leaking
log("ABR Enabled: " + USE_ABR_VIDEOS); log("ABR Enabled: " + USE_ABR_VIDEOS);
const defaultUMP = USE_ABR_VIDEOS || forceUmp;
url = convertIfOtherUrl(url); url = convertIfOtherUrl(url);
...@@ -407,10 +414,11 @@ else { ...@@ -407,10 +414,11 @@ else {
} }
let batchIOS = -1; 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++; batchIOS = batchCounter++;
} }*/
const resps = batch.execute(); const resps = batch.execute();
...@@ -423,7 +431,15 @@ else { ...@@ -423,7 +431,15 @@ else {
let initialData = getInitialData(html); let initialData = getInitialData(html);
let initialPlayerData = getInitialPlayerData(html); let initialPlayerData = getInitialPlayerData(html);
let clientConfig = getClientConfig(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; let ageRestricted = initialPlayerData.playabilityStatus?.reason?.indexOf("your age") > 0 ?? false;
if (initialPlayerData.playabilityStatus?.status == "LOGIN_REQUIRED" && (bridge.isLoggedIn() || !ageRestricted)) { if (initialPlayerData.playabilityStatus?.status == "LOGIN_REQUIRED" && (bridge.isLoggedIn() || !ageRestricted)) {
if(!!_settings?.allowLoginFallback && !useLogin) { if(!!_settings?.allowLoginFallback && !useLogin) {
...@@ -434,6 +450,7 @@ else { ...@@ -434,6 +450,7 @@ else {
initialData = getInitialData(html); initialData = getInitialData(html);
initialPlayerData = getInitialPlayerData(html); initialPlayerData = getInitialPlayerData(html);
clientConfig = getClientConfig(html); clientConfig = getClientConfig(html);
usedLogin = true && bridge.isLoggedIn();
if (initialPlayerData.playabilityStatus?.status == "LOGIN_REQUIRED") if (initialPlayerData.playabilityStatus?.status == "LOGIN_REQUIRED")
throw new ScriptLoginRequiredException("Login required\nReason: " + initialPlayerData?.playabilityStatus?.reason); throw new ScriptLoginRequiredException("Login required\nReason: " + initialPlayerData?.playabilityStatus?.reason);
...@@ -516,9 +533,9 @@ else { ...@@ -516,9 +533,9 @@ else {
jsUrl: jsUrl jsUrl: jsUrl
}; };
const videoDetails = extractVideoPage_VideoDetails(initialData, initialPlayerData, { const videoDetails = extractVideoPage_VideoDetails(url, initialData, initialPlayerData, {
url: url url: url
}, jsUrl, useLogin, USE_ABR_VIDEOS, clientConfig); }, jsUrl, useLogin, defaultUMP, clientConfig, usedLogin);
if(videoDetails == null) if(videoDetails == null)
throw new UnavailableException("No video found"); throw new UnavailableException("No video found");
...@@ -533,6 +550,9 @@ else { ...@@ -533,6 +550,9 @@ else {
throw new UnavailableException("No sources found"); throw new UnavailableException("No sources found");
} }
let bgData = getBGDataFromClientConfig(clientConfig, usedLogin);
//Substitute Dash manifest from Android //Substitute Dash manifest from Android
if(USE_ANDROID_FALLBACK && videoDetails.dash && videoDetails.dash.url) { if(USE_ANDROID_FALLBACK && videoDetails.dash && videoDetails.dash.url) {
const androidData = requestAndroidStreamingData(videoDetails.id.value); const androidData = requestAndroidStreamingData(videoDetails.id.value);
...@@ -573,7 +593,7 @@ else { ...@@ -573,7 +593,7 @@ else {
else if(USE_IOS_VIDEOS_FALLBACK && !USE_ABR_VIDEOS && !simplify) { else if(USE_IOS_VIDEOS_FALLBACK && !USE_ABR_VIDEOS && !simplify) {
const iosDataResp = (batchIOS > 0) ? const iosDataResp = (batchIOS > 0) ?
resps[batchIOS] : resps[batchIOS] :
requestIOSStreamingData(videoDetails.id.value); requestIOSStreamingData(videoDetails.id.value, undefined, getBGDataFromClientConfig(clientConfig, usedLogin), usedLogin);
if(iosDataResp.isOk) { if(iosDataResp.isOk) {
const iosData = JSON.parse(iosDataResp.body); const iosData = JSON.parse(iosDataResp.body);
if(IS_TESTING) if(IS_TESTING)
...@@ -589,14 +609,14 @@ else { ...@@ -589,14 +609,14 @@ else {
log("Failed to get iOS stream data, fallback to UMP") log("Failed to get iOS stream data, fallback to UMP")
if(!!_settings["showVerboseToasts"]) if(!!_settings["showVerboseToasts"])
bridge.toast("Failed to get iOS stream data, fallback to UMP"); 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 { else {
log("Failed to get iOS stream data, fallback to UMP (" + iosDataResp?.code + ")") log("Failed to get iOS stream data, fallback to UMP (" + iosDataResp?.code + ")")
if(!!_settings["showVerboseToasts"]) if(!!_settings["showVerboseToasts"])
bridge.toast("Failed to get iOS stream data, fallback to UMP"); 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 { ...@@ -616,6 +636,13 @@ else {
} }
const finalResult = videoDetails; const finalResult = videoDetails;
finalResult.bgData = bgData;
if(_setMetadata) {
finalResult.metaData = {
"initialData": JSON.stringify(initialData)
}
}
finalResult.__initialData = initialData; finalResult.__initialData = initialData;
if(!!_settings["youtubeActivity"] && useLogin) { if(!!_settings["youtubeActivity"] && useLogin) {
finalResult.__playerData = initialPlayerData; finalResult.__playerData = initialPlayerData;
...@@ -634,6 +661,18 @@ else { ...@@ -634,6 +661,18 @@ else {
return source.getContentRecommendations(url, initialData); 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; return finalResult;
}; };
} }
...@@ -1272,8 +1311,8 @@ source.getChannel = (url) => { ...@@ -1272,8 +1311,8 @@ source.getChannel = (url) => {
source.getChannelCapabilities = () => { source.getChannelCapabilities = () => {
return { return {
types: [Type.Feed.Videos, Type.Feed.Streams], types: (!!_settings?.channelRssOnly) ? [Type.Feed.Mixed] : [Type.Feed.Videos, Type.Feed.Streams],
sorts: [Type.Order.Chronological, "Popular"] sorts: (!!_settings?.channelRssOnly) ? [Type.Order.Chronological] : [Type.Order.Chronological, "Popular"]//
}; };
} }
function filterChannelUrl(url) { function filterChannelUrl(url) {
...@@ -1300,6 +1339,12 @@ source.getChannelContents = (url, type, order, filters) => { ...@@ -1300,6 +1339,12 @@ source.getChannelContents = (url, type, order, filters) => {
let targetTab = null; let targetTab = null;
url = filterChannelUrl(url); url = filterChannelUrl(url);
if(!!_settings?.channelRssOnly) {
log("Using Channel RSS Only");
return new VideoPager(source.peekChannelContents(url, type, true), false);
}
log("GetChannelContents - " + type); log("GetChannelContents - " + type);
switch(type) { switch(type) {
case undefined: case undefined:
...@@ -1387,10 +1432,15 @@ source.getChannelPlaylists = (url) => { ...@@ -1387,10 +1432,15 @@ source.getChannelPlaylists = (url) => {
source.getPeekChannelTypes = () => { source.getPeekChannelTypes = () => {
return [Type.Feed.Videos, Type.Feed.Mixed]; 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) if(type != Type.Feed.Mixed && type != Type.Feed.Videos)
return []; 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); const match = url.match(REGEX_VIDEO_CHANNEL_URL);
if(!match || match.length != 3) if(!match || match.length != 3)
return {}; return {};
...@@ -1883,8 +1933,9 @@ class YTVideoSource extends VideoUrlRangeSource { ...@@ -1883,8 +1933,9 @@ class YTVideoSource extends VideoUrlRangeSource {
} }
class YTABRVideoSource extends DashManifestRawSource { class YTABRVideoSource extends DashManifestRawSource {
constructor(obj, url, sourceObj, ustreamerConfig, bgData) { constructor(itag, obj, url, sourceObj, ustreamerConfig, bgData, parentUrl, usedLogin) {
super(obj); super(obj);
this.itag = itag;
this.url = url; this.url = url;
this.abrUrl = url; this.abrUrl = url;
this.ustreamerConfig = ustreamerConfig; this.ustreamerConfig = ustreamerConfig;
...@@ -1896,14 +1947,17 @@ class YTABRVideoSource extends DashManifestRawSource { ...@@ -1896,14 +1947,17 @@ class YTABRVideoSource extends DashManifestRawSource {
this.bgData = bgData; this.bgData = bgData;
this.visitorId = bgData.visitorData; this.visitorId = bgData.visitorData;
this.dataSyncId = bgData.dataSyncId this.dataSyncId = bgData.dataSyncId
this.visitorDataType = bgData.visitorDataType;
this.parentUrl = parentUrl;
this.usedLogin = !!usedLogin;
} }
generate() { generate() {
if(this.lastDash) if(this.lastDash)
return this.lastDash; return this.lastDash;
log("Generating ABR Video Dash"); log("Generating ABR Video Dash for " + this.sourceObj.itag);
getMediaReusableVideoBuffers()?.freeAll(); 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.initialHeader = fileHeader;
this.initialUMP = umpResp; this.initialUMP = umpResp;
this.lastDash = dash; this.lastDash = dash;
...@@ -1916,14 +1970,15 @@ class YTABRVideoSource extends DashManifestRawSource { ...@@ -1916,14 +1970,15 @@ class YTABRVideoSource extends DashManifestRawSource {
return dash; return dash;
} }
getRequestExecutor() { getRequestExecutor() {
return new YTABRExecutor(this.abrUrl, this.sourceObj, this.ustreamerConfig, return new YTABRExecutor(this, this.abrUrl, this.sourceObj, this.ustreamerConfig,
this.initialHeader, this.initialHeader,
this.initialUMP, this.bgData); this.initialUMP, this.bgData);
} }
} }
class YTABRAudioSource extends DashManifestRawAudioSource { class YTABRAudioSource extends DashManifestRawAudioSource {
constructor(obj, url, sourceObj, ustreamerConfig, bgData) { constructor(itag, obj, url, sourceObj, ustreamerConfig, bgData, parentUrl, usedLogin) {
super(obj); super(obj);
this.itag = itag;
this.url = url; this.url = url;
this.abrUrl = url; this.abrUrl = url;
this.ustreamerConfig = ustreamerConfig; this.ustreamerConfig = ustreamerConfig;
...@@ -1933,6 +1988,9 @@ class YTABRAudioSource extends DashManifestRawAudioSource { ...@@ -1933,6 +1988,9 @@ class YTABRAudioSource extends DashManifestRawAudioSource {
this.bgData = bgData; this.bgData = bgData;
this.visitorId = bgData.visitorData; this.visitorId = bgData.visitorData;
this.dataSyncId = bgData.dataSyncId this.dataSyncId = bgData.dataSyncId
this.visitorDataType = bgData.visitorDataType;
this.parentUrl = parentUrl;
this.usedLogin = !!usedLogin;
} }
generate() { generate() {
...@@ -1940,7 +1998,7 @@ class YTABRAudioSource extends DashManifestRawAudioSource { ...@@ -1940,7 +1998,7 @@ class YTABRAudioSource extends DashManifestRawAudioSource {
return this.lastDash; return this.lastDash;
log("Generating ABR Audio Dash"); log("Generating ABR Audio Dash");
getMediaReusableAudioBuffers()?.freeAll(); 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.initialHeader = fileHeader;
this.initialUMP = umpResp; this.initialUMP = umpResp;
this.lastDash = dash; this.lastDash = dash;
...@@ -1953,23 +2011,43 @@ class YTABRAudioSource extends DashManifestRawAudioSource { ...@@ -1953,23 +2011,43 @@ class YTABRAudioSource extends DashManifestRawAudioSource {
return dash; return dash;
} }
getRequestExecutor() { getRequestExecutor() {
return new YTABRExecutor(this.abrUrl, this.sourceObj, this.ustreamerConfig, return new YTABRExecutor(this, this.abrUrl, this.sourceObj, this.ustreamerConfig,
this.initialHeader, this.initialHeader,
this.initialUMP, this.bgData); this.initialUMP, this.bgData);
} }
} }
function generateDash(sourceObj, ustreamerConfig, abrUrl, itag) { function generateDash(parentSource, sourceObj, ustreamerConfig, abrUrl, itag) {
const now = (new Date()).getTime(); const now = (new Date()).getTime();
const lastAction = (new Date()).getTime() - (Math.random() * 5000); 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(); const postData = initialReq.serializeBinary();
let initialResp = http.POST(abrUrl, postData, { let initialResp = http.POST(abrUrl, postData, {
"Origin": "https://www.youtube.com", "Origin": "https://www.youtube.com",
"Accept": "*/*", "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); }, false, true);
if(!initialResp.isOk) if(!initialResp.isOk) {
throw new ScriptException("Failed initial stream request [ " + initialResp.code + "]"); throw new ScriptException("Failed initial stream request [ " + initialResp.code + "]");
}
const data = initialResp.body; const data = initialResp.body;
let byteArray = undefined; let byteArray = undefined;
...@@ -2030,25 +2108,39 @@ function generateDash(sourceObj, ustreamerConfig, abrUrl, itag) { ...@@ -2030,25 +2108,39 @@ function generateDash(sourceObj, ustreamerConfig, abrUrl, itag) {
streams.push(stream); streams.push(stream);
} }
const webmHeaderData = streams[0].data; const headerData = streams[0].data;
const webmHeader = new WEBMHeader(webmHeaderData, let header = undefined;
sourceObj.mimeType.split(";")[0], if(sourceObj.mimeType.indexOf("webm") > 0) {
/codecs=\"(.+)\"/.exec(sourceObj.mimeType)[1], const webmHeader = new WEBMHeader(headerData, sourceObj.mimeType.split(";")[0], /codecs=\"(.+)\"/.exec(sourceObj.mimeType)[1], sourceObj.width, sourceObj.height);
sourceObj.width, const urlPrefix = (isVideo) ?
sourceObj.height); "https://grayjay.internal/video" :
const urlPrefix = (isVideo) ? "https://grayjay.internal/audio";
"https://grayjay.internal/video" : const dash = generateWEBMDash(webmHeader,
"https://grayjay.internal/audio"; urlPrefix + "/internal/segment.webm?segIndex=$Number$",
const dash = generateWEBMDash(webmHeader, urlPrefix + "/internal/init.webm");
urlPrefix + "/internal/segment.webm?segIndex=$Number$",
urlPrefix + "/internal/init.webm"); return [dash, umpResp, webmHeader];
}
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) { function generateWEBMDash(webm, templateUrl, initUrl) {
const duration = splitMS(webm.duration); 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 repCounter = 1;
let mpd = `<?xml version="1.0" encoding="UTF-8"?>\n`; let mpd = `<?xml version="1.0" encoding="UTF-8"?>\n`;
...@@ -2076,12 +2168,13 @@ function generateWEBMDash(webm, templateUrl, initUrl) { ...@@ -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 + " ") xmlTag("S", {t: cue, d: (webm.cues.length > i + 1) ? webm.cues[i + 1] - cue : webm.durationCueTimescale - cue}, undefined, indent + " ")
).join("") ).join("")
,indent + " ") ,indent + " ")
,indent + " ") ,indent + " ")
+ " \n"//TEMPORARY FIX, FIX MATCH REPLACEMENT
,indent + " ") ,indent + " ")
, indent + " ") , indent + " ")
, indent + " ") , indent + " ")
, ""); , "");
log(mpd);
return mpd; return mpd;
} }
function splitMS(ms) { function splitMS(ms) {
...@@ -2208,8 +2301,10 @@ const useReusableBuffers = false; ...@@ -2208,8 +2301,10 @@ const useReusableBuffers = false;
let executorCounter = 0; let executorCounter = 0;
let _executorsVideo = []; let _executorsVideo = [];
let _executorsAudio = []; let _executorsAudio = [];
let _recoveryCache = {};
class YTABRExecutor { class YTABRExecutor {
constructor(url, source, ustreamerConfig, header, initialUmp, bgData) { constructor(parentSource, url, source, ustreamerConfig, header, initialUmp, bgData) {
this.parentSource = parentSource;
this.executorId = executorCounter++; this.executorId = executorCounter++;
this.source = source; this.source = source;
this.itag = source.itag; this.itag = source.itag;
...@@ -2222,14 +2317,23 @@ class YTABRExecutor { ...@@ -2222,14 +2317,23 @@ class YTABRExecutor {
this.requestStarted = (new Date()).getTime(); this.requestStarted = (new Date()).getTime();
this.lastAction = (new Date()).getTime() - (Math.random() * 1000 * 5); this.lastAction = (new Date()).getTime() - (Math.random() * 1000 * 5);
this.segmentOffsets = undefined; 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)=>{ tryGetBotguard((bg)=>{
bg.getTokenOrCreate(bgData.visitorData, bgData.dataSyncId, (pot)=>{ bg.getTokenOrCreate(bgData.visitorData, bgData.dataSyncId, (pot)=>{
console.log("Botguard Token to use:", pot); console.log("Botguard Token to use:", pot);
this.pot = pot; this.pot = pot;
}); }, bgData.visitorDataType);
}); });
}
log("UMP New executor: " + source.name + " - " + source.mimeType + " (segments: " + header?.cues?.length + ")"); log("UMP New executor: " + source.name + " - " + source.mimeType + " (segments: " + header?.cues?.length + ")");
log("UMP Cues: " + header?.cues?.join(", ")); log("UMP Cues: " + header?.cues?.join(", "));
...@@ -2279,6 +2383,8 @@ class YTABRExecutor { ...@@ -2279,6 +2383,8 @@ class YTABRExecutor {
log("UMP Cues: " + this.header.cues.join(", ")); log("UMP Cues: " + this.header.cues.join(", "));
throw new ScriptException("Zero time for non-zero segment?"); throw new ScriptException("Zero time for non-zero segment?");
} }
if(this.header.cueTimeScale)
return parseInt((time / this.header.cueTimeScale) * 1000);
return time; return time;
} }
else else
...@@ -2317,11 +2423,16 @@ class YTABRExecutor { ...@@ -2317,11 +2423,16 @@ class YTABRExecutor {
const reusable = this.reusableBuffer; const reusable = this.reusableBuffer;
for(let key of Object.keys(this.segments)) { for(let key of Object.keys(this.segments)) {
reusable?.free(this.segments[key].data); reusable?.free(this.segments[key].data);
const buffer = this.segments[key]?.data?.buffer;
if(buffer && !buffer.detached)
buffer?.transfer();
delete this.segments[key]; delete this.segments[key];
} }
} }
cleanup() { cleanup() {
if(this.childExecutor)
return this.childExecutor.cleanup();
log("UMP: Cleaning up!"); log("UMP: Cleaning up!");
this.initialUmp = undefined; this.initialUmp = undefined;
this.header = undefined; this.header = undefined;
...@@ -2337,9 +2448,46 @@ class YTABRExecutor { ...@@ -2337,9 +2448,46 @@ class YTABRExecutor {
log("Remaining audio executors: " + _executorsAudio.length); log("Remaining audio executors: " + _executorsAudio.length);
} }
this.freeAllSegments(); 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) { executeRequest(url, headers, retryCount, overrideSegment) {
console.clear();
if(this.childExecutor)
return this.childExecutor.executeRequest(url, headers, retryCount, overrideSegment);
if(!retryCount) if(!retryCount)
retryCount = 0; retryCount = 0;
log("UMP: " + url + ""); log("UMP: " + url + "");
...@@ -2365,11 +2513,11 @@ class YTABRExecutor { ...@@ -2365,11 +2513,11 @@ class YTABRExecutor {
log("UMP [" + this.type + "] Cached segment " + segment + " was undefined, refetching"); 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) if(overrideSegment)
log("UMP [" + this.type + "] requesting with overrided segment: " + overrideSegment) log("UMP [" + this.type + "] requesting with overrided segment: " + overrideSegment)
const now = (new Date()).getTime(); 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 initialReq = getVideoPlaybackRequest(this.source, this.ustreamerConfig, time, (overrideSegment) ? overrideSegment : segment, this.lastRequest, this.lastAction, now, this.playbackCookie, pot);
const postData = initialReq.serializeBinary(); const postData = initialReq.serializeBinary();
const initialResp = http.POST(this.abrUrl, postData, { const initialResp = http.POST(this.abrUrl, postData, {
...@@ -2412,10 +2560,41 @@ class YTABRExecutor { ...@@ -2412,10 +2560,41 @@ class YTABRExecutor {
this.lastRequest = (new Date()).getTime(); this.lastRequest = (new Date()).getTime();
const stream = streamsArr[0]; 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) : "")); throw new ScriptException("No streams for requesting segment " + segment + ((overrideSegment && overrideSegment > 0) ? (", override: " + overrideSegment) : ""));
}
const expectedSegment = parseInt(segment) + parseInt(this.getOffset(stream.segmentIndex)); const expectedSegment = parseInt(segment) + parseInt(this.getOffset(stream.segmentIndex));
log("Expected segment " + expectedSegment + " got " + stream.segmentIndex); log("Expected segment " + expectedSegment + " got " + stream.segmentIndex);
if(stream && stream.segmentIndex != expectedSegment) { if(stream && stream.segmentIndex != expectedSegment) {
...@@ -2470,6 +2649,10 @@ class YTABRExecutor { ...@@ -2470,6 +2649,10 @@ class YTABRExecutor {
if(!stream || !stream.data) if(!stream || !stream.data)
throw new ScriptException("NO STREAMDATA FOUND (" + Object.keys(umpResp.streams).join(", ") + "): " + !!umpResp.streams[0]?.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); log("UMP [" + this.type + "]: segment " + segment + " - " + stream.data?.length);
return stream.data; return stream.data;
} }
...@@ -2481,7 +2664,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn ...@@ -2481,7 +2664,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn
const clientInfo = new pb.VideoPlaybackRequest_pb.ClientInfo(); const clientInfo = new pb.VideoPlaybackRequest_pb.ClientInfo();
clientInfo.setClientname(1); clientInfo.setClientname(1);
clientInfo.setClientversion("2.20250107.01.00"); clientInfo.setClientversion("2.20250131.01.00");
clientInfo.setOsname("Windows"); clientInfo.setOsname("Windows");
clientInfo.setOsversion("10.0"); clientInfo.setOsversion("10.0");
...@@ -2494,12 +2677,12 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn ...@@ -2494,12 +2677,12 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn
info.setVideoheight2maybe(source.height); info.setVideoheight2maybe(source.height);
info.setSelectedqualityheight(source.height); info.setSelectedqualityheight(source.height);
} }
info.setG7(8613683); info.setG7(104857); //x
info.setCurrentvideopositionms(playerPosMs); info.setCurrentvideopositionms(playerPosMs); //x
if(lastRequest > 0) if(lastRequest > 0)
info.setTimesincelastrequestms((new Date().getTime() - lastRequest)); info.setTimesincelastrequestms((new Date().getTime() - lastRequest)); //x
info.setTimesincelastactionms(Math.floor((new Date()).getTime() - lastAction)); info.setTimesincelastactionms(Math.floor((new Date()).getTime() - lastAction)); //x
info.setDynamicrangecompression(true); info.setDynamicrangecompression(true); //x
info.setLatencymsmaybe(Math.floor(Math.random() * 90 + 7)); info.setLatencymsmaybe(Math.floor(Math.random() * 90 + 7));
info.setLastmanualdirection(0); info.setLastmanualdirection(0);
info.setTimesincelastmanualformatselectionms(requestStarted); info.setTimesincelastmanualformatselectionms(requestStarted);
...@@ -2523,6 +2706,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn ...@@ -2523,6 +2706,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn
if(source.xtags) if(source.xtags)
format.setXtags(source.xtags); format.setXtags(source.xtags);
if(segmentIndex > 0) { if(segmentIndex > 0) {
const bufferedStream = new pb.VideoPlaybackRequest_pb.BufferedStreamInfo() const bufferedStream = new pb.VideoPlaybackRequest_pb.BufferedStreamInfo()
bufferedStream.setFormatid(format); bufferedStream.setFormatid(format);
...@@ -2530,7 +2714,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn ...@@ -2530,7 +2714,7 @@ function getVideoPlaybackRequest(source, ustreamerConfig, playerPosMs, segmentIn
bufferedStream.setBufferedsegmentstartindex(1); bufferedStream.setBufferedsegmentstartindex(1);
bufferedStream.setBufferedsegmentendindex(segmentIndex - 1); bufferedStream.setBufferedsegmentendindex(segmentIndex - 1);
bufferedStream.setBufferedstarttimems(0); bufferedStream.setBufferedstarttimems(0);
//bufferedStream.setBuffereddurationms(playerPosMs); bufferedStream.setBuffereddurationms(playerPosMs);
vidReq.setBufferedstreamsList[bufferedStream]; vidReq.setBufferedstreamsList[bufferedStream];
vidReq.setDesiredstreamsList([format]); vidReq.setDesiredstreamsList([format]);
} }
...@@ -3263,7 +3447,7 @@ function requestClientConfig(useMobile = false, useAuth = false) { ...@@ -3263,7 +3447,7 @@ function requestClientConfig(useMobile = false, useAuth = false) {
return getClientConfig(resp.body); return getClientConfig(resp.body);
} }
function requestIOSStreamingData(videoId, batch) { function requestIOSStreamingData(videoId, batch, visitorData, useLogin) {
const body = { const body = {
videoId: videoId, videoId: videoId,
cpn: "" + randomString(16), cpn: "" + randomString(16),
...@@ -3280,12 +3464,23 @@ function requestIOSStreamingData(videoId, batch) { ...@@ -3280,12 +3464,23 @@ function requestIOSStreamingData(videoId, batch) {
"osVersion": IOS_OS_VERSION_DETAILED,//"15.6.0.19G71",^M "osVersion": IOS_OS_VERSION_DETAILED,//"15.6.0.19G71",^M
"hl": langDisplay, "hl": langDisplay,
"gl": langRegion, "gl": langRegion,
"utcOffsetMinutes": 0
}, },
user: { user: {
"lockedSafetyMode": false "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 = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"User-Agent": USER_AGENT_IOS, "User-Agent": USER_AGENT_IOS,
...@@ -3301,11 +3496,11 @@ function requestIOSStreamingData(videoId, batch) { ...@@ -3301,11 +3496,11 @@ function requestIOSStreamingData(videoId, batch) {
"&id=" + videoId "&id=" + videoId
if(batch) { if(batch) {
batch.POST(url, JSON.stringify(body), headers, false); batch.POST(url, JSON.stringify(body), headers, !!useLogin);
return null; return null;
} }
else { else {
const resp = http.POST(url, JSON.stringify(body), headers, false); const resp = http.POST(url, JSON.stringify(body), headers, !!useLogin);
return resp; return resp;
} }
} }
...@@ -3728,7 +3923,7 @@ function extractPage_Tabs(initialData, contextData) { ...@@ -3728,7 +3923,7 @@ function extractPage_Tabs(initialData, contextData) {
//#region Layout Extractors //#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 contents = initialData.contents;
const contentsContainer = contents.twoColumnWatchNextResults?.results?.results ?? const contentsContainer = contents.twoColumnWatchNextResults?.results?.results ??
null; null;
...@@ -3770,7 +3965,7 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa ...@@ -3770,7 +3965,7 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa
video: video:
((!useAbr) ? ((!useAbr) ?
extractAdaptiveFormats_VideoDescriptor(initialPlayerData?.streamingData?.adaptiveFormats, jsUrl, contextData, "") : extractAdaptiveFormats_VideoDescriptor(initialPlayerData?.streamingData?.adaptiveFormats, jsUrl, contextData, "") :
extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig) extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig, parentUrl, usedLogin)
) )
?? new VideoSourceDescriptor([]), ?? new VideoSourceDescriptor([]),
subtitles: initialPlayerData subtitles: initialPlayerData
...@@ -3943,7 +4138,32 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa ...@@ -3943,7 +4138,32 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa
return result; 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) ? const abrStreamingUrl = (initialPlayerData.streamingData.serverAbrStreamingUrl) ?
decryptUrlN(initialPlayerData.streamingData.serverAbrStreamingUrl, jsUrl, false) : undefined; decryptUrlN(initialPlayerData.streamingData.serverAbrStreamingUrl, jsUrl, false) : undefined;
...@@ -3952,43 +4172,50 @@ function extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clien ...@@ -3952,43 +4172,50 @@ function extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clien
return new UnMuxVideoSourceDescriptor( return new UnMuxVideoSourceDescriptor(
(initialPlayerData.streamingData.adaptiveFormats (initialPlayerData.streamingData.adaptiveFormats
.filter(x => x.mimeType.startsWith("video/webm")) .filter(x => x.mimeType.startsWith("video/webm") || x.mimeType.startsWith("video/mp4"))
.map(y => { .map(y => {
const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1); const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1);
const container = y.mimeType.substring(0, y.mimeType.indexOf(';')); 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. return null; //AV01 is unsupported.
const duration = parseInt(parseInt(y.approxDurationMs) / 1000) ?? 0; const duration = parseInt(parseInt(y.approxDurationMs) / 1000) ?? 0;
if (isNaN(duration)) if (isNaN(duration))
return null; return null;
console.log("VisitorData: ", clientConfig?.EOM_VISITOR_DATA); if(isAV1)
return new YTABRVideoSource({ log("FOUND AV1: " + "UMP " + y.height + "p" + (y.fps ? y.fps : "") + " " + container + ((isAV1) ? " [AV1]" : ""));
name: "UMP " + y.height + "p" + (y.fps ? y.fps : "") + " " + container, const result = new YTABRVideoSource(y.itag, {
name: "UMP " + y.height + "p" + (y.fps ? y.fps : "") + " " + container + ((isAV1) ? " [AV1]" : ""),
url: abrStreamingUrl, url: abrStreamingUrl,
width: y.width, width: y.width,
height: y.height, height: y.height,
duration: (!isNaN(duration)) ? duration : 0, duration: (!isNaN(duration)) ? duration : 0,
container: y.mimeType.substring(0, y.mimeType.indexOf(';')), container: y.mimeType.substring(0, y.mimeType.indexOf(';')),
codec: codecs, codec: codecs,
bitrate: y.bitrate, bitrate: y.bitrate
}, abrStreamingUrl, y, initialPlayerData.playerConfig.mediaCommonConfig.mediaUstreamerRequestConfig.videoPlaybackUstreamerConfig, }, 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), })).filter(x => x != null),
//Audio //Audio
(initialPlayerData.streamingData.adaptiveFormats (initialPlayerData.streamingData.adaptiveFormats
.filter(x => x.mimeType.startsWith("audio/webm")) .filter(x => x.mimeType.startsWith("audio/webm") || x.mimeType.startsWith("audio/mp4"))
.map(y => { .map(y => {
const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1); const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1);
const container = y.mimeType.substring(0, y.mimeType.indexOf(';')); 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. return null; //AV01 is unsupported.
const duration = parseInt(parseInt(y.approxDurationMs) / 1000) ?? 0; const duration = parseInt(parseInt(y.approxDurationMs) / 1000) ?? 0;
if (isNaN(duration)) if (isNaN(duration))
return null; return null;
return new YTABRAudioSource({ return new YTABRAudioSource(y.itag, {
name: "UMP " + (y.audioTrack?.displayName ? y.audioTrack.displayName : codecs), name: "UMP " + (y.audioTrack?.displayName ? y.audioTrack.displayName : codecs) + ((isAV1) ? " [AV1]" : ""),
url: abrStreamingUrl, url: abrStreamingUrl,
width: y.width, width: y.width,
height: y.height, height: y.height,
...@@ -3999,7 +4226,7 @@ function extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clien ...@@ -3999,7 +4226,7 @@ function extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clien
audioChannels: y.audioChannels, audioChannels: y.audioChannels,
language: ytLangIdToLanguage(y.audioTrack?.id) language: ytLangIdToLanguage(y.audioTrack?.id)
}, abrStreamingUrl, y, initialPlayerData.playerConfig.mediaCommonConfig.mediaUstreamerRequestConfig.videoPlaybackUstreamerConfig, }, 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) })).filter(x => x != null)
); );
} }
...@@ -5559,6 +5786,10 @@ source.decryptUrlTestN = function(n) { ...@@ -5559,6 +5786,10 @@ source.decryptUrlTestN = function(n) {
return decryptUrlN(url, true); return decryptUrlN(url, true);
} }
source.decryptUrlN = function(url, jsUrl) {
prepareCipher(jsUrl);
return decryptUrlN(url, jsUrl, true);
}
function decryptUrl(encrypted, jsUrl, doLogging) { function decryptUrl(encrypted, jsUrl, doLogging) {
if(!encrypted) return null; if(!encrypted) return null;
...@@ -5740,7 +5971,7 @@ function getNDecryptorFunctionCode(code, jsUrl) { ...@@ -5740,7 +5971,7 @@ function getNDecryptorFunctionCode(code, jsUrl) {
throw new ScriptException("Failed to find n decryptor (code)\n" + 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 = []; const typeChecks = [];
let prefix = ""; let prefix = "";
let typeCheck = undefined; let typeCheck = undefined;
...@@ -5900,6 +6131,11 @@ class UMPResponse { ...@@ -5900,6 +6131,11 @@ class UMPResponse {
if(stream22) if(stream22)
stream22.completed = true; stream22.completed = true;
break; break;
case 29: //Unknown
const opCode29 = pb.Opcode29_pb.Opcode29.deserializeBinary(segment);
console.log("");
break;
case 35://Opcode35: Playbackcookie case 35://Opcode35: Playbackcookie
const opCode35 = pb.Opcode35_pb.Opcode35.deserializeBinary(segment); const opCode35 = pb.Opcode35_pb.Opcode35.deserializeBinary(segment);
this.playbackCookie = opCode35.getPlaybackcookie(); this.playbackCookie = opCode35.getPlaybackcookie();
...@@ -5907,6 +6143,13 @@ class UMPResponse { ...@@ -5907,6 +6143,13 @@ class UMPResponse {
case 43://Message case 43://Message
const opCode43 = pb.Opcode43_pb.Opcode43.deserializeBinary(segment); const opCode43 = pb.Opcode43_pb.Opcode43.deserializeBinary(segment);
this.redirectUrl = opCode43?.getRedirecturl(); 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 { else {
...@@ -5916,7 +6159,7 @@ class UMPResponse { ...@@ -5916,7 +6159,7 @@ class UMPResponse {
} }
} }
if(this.streamCount == 0) { 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) { if(bytes.length < 200) {
log("UMP Resp: " + bytes.join(" ") + "\nOpcodes: " + this.opcodes.map(x=>`${x.opcode}:${x.length}`).join(", ")); log("UMP Resp: " + bytes.join(" ") + "\nOpcodes: " + this.opcodes.map(x=>`${x.opcode}:${x.length}`).join(", "));
return undefined; return undefined;
...@@ -6038,17 +6281,6 @@ function binaryReadFloat(bytes, pointer, size) { ...@@ -6038,17 +6281,6 @@ function binaryReadFloat(bytes, pointer, size) {
//#endregion //#endregion
//#region MP4
class MP4Header {
constructor(bytes) {
}
}
//#endregion
let pb = {}; let pb = {};
source.testProtobuf = function() { source.testProtobuf = function() {
let test2 = new pb.VideoPlaybackRequest_pb.VideoPlaybackRequestInfo(); let test2 = new pb.VideoPlaybackRequest_pb.VideoPlaybackRequestInfo();
...@@ -6084,24 +6316,110 @@ source.testing = function(url) { ...@@ -6084,24 +6316,110 @@ source.testing = function(url) {
return generated; return generated;
} }
source.testUMP = async function(url, startSegment, endSegment, loops = 2){ source.testUMPRecovery = async function(){
const url = ""
USE_ABR_VIDEOS = true; USE_ABR_VIDEOS = true;
const item = this.getContentDetails(url); const item = this.getContentDetails(url);
console.log(item); 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++) { let failed = false;
console.log("Loop: " + i); 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(); const generated = video.generate();
console.log("Generated:", generated);
const executor = video.getRequestExecutor(); const executor = video.getRequestExecutor();
if(endSegment && endSegment > 0) {
for(let i = startSegment; i < endSegment; i++) { for(let i = startSegment; i < endSegment; i++) {
executor.executeRequest("https://grayjay.app/internal/video?segIndex=" + i, {}); const resp = executor.executeRequest("https://grayjay.app/internal/video?segIndex=" + i, {});
await delay(2000); 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; return;
}; };
const delay = (delayInms) => { const delay = (delayInms) => {
...@@ -6110,6 +6428,132 @@ const delay = (delayInms) => { ...@@ -6110,6 +6428,132 @@ const delay = (delayInms) => {
console.log("LOADED"); 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 //#region WEBM
class WEBMHeader { class WEBMHeader {
...@@ -6292,6 +6736,9 @@ function tryGetBotguard(cb) { ...@@ -6292,6 +6736,9 @@ function tryGetBotguard(cb) {
cb(botguard); cb(botguard);
}, 100); }, 100);
} }
function getExistingBotguard(){
return existingBotguard;
}
//#region BotGuard //#region BotGuard
class BotGuardGeneratorInput { class BotGuardGeneratorInput {
...@@ -6321,6 +6768,10 @@ class BotGuardGenerator { ...@@ -6321,6 +6768,10 @@ class BotGuardGenerator {
} }
initialize() { initialize() {
this.mint = undefined;
this.mintConstructor = undefined;
this.minterFailure = undefined;
this.ready = false;
console.log("VM Initializing"); console.log("VM Initializing");
const requestKey = this.requestKey; const requestKey = this.requestKey;
...@@ -6449,6 +6900,14 @@ class BotGuardGenerator { ...@@ -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() { constructMinter() {
if (!this.mintConstructor) if (!this.mintConstructor)
throw "No Mint Constructor"; throw "No Mint Constructor";
...@@ -6483,33 +6942,88 @@ class BotGuardGenerator { ...@@ -6483,33 +6942,88 @@ class BotGuardGenerator {
return this.mint; 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[idToUse];
const existing = this.generatedTokens[visitorId];
if(!existing) { if(!existing) {
log("No existing botguard token, generating new"); log("No existing botguard token, generating new");
this.generateBase64(visitorId, dataSync, (token)=>{ this.generateBase64(visitorId, dataSyncId, (token)=>{
cb(token); cb(token);
}); }, type);
} }
else //TODO: check expiry? else //TODO: check expiry?
cb(existing.tokenBase64); cb(existing.tokenBase64);
} }
generateBase64(visitorId, dataSync, cb) { generateBase64(visitorId, dataSyncId, cb, type) {
this.generate(visitorId, dataSync, (result) => { this.generate(visitorId, dataSyncId, (result) => {
const originId = visitorId; const originId = visitorId;
let poToken = btoa(String.fromCharCode(...result)) let poToken = btoa(String.fromCharCode(...result))
.replace(/\+/g, '-') .replace(/\+/g, '-')
.replace(/\//g, '_'); .replace(/\//g, '_');
cb(poToken); 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) => { this.getMinter((minter, expire) => {
console.log("Minting visitor: ", visitorId); try {
const poToken = minter(new TextEncoder().encode(visitorId)); 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)) const poTokenBase64 = btoa(String.fromCharCode(...poToken))
.replace(/\+/g, '-') .replace(/\+/g, '-')
.replace(/\//g, '_'); .replace(/\//g, '_');
...@@ -6518,14 +7032,17 @@ class BotGuardGenerator { ...@@ -6518,14 +7032,17 @@ class BotGuardGenerator {
const newPoToken = { const newPoToken = {
token: poToken, token: poToken,
tokenBase64: poTokenBase64, tokenBase64: poTokenBase64,
expires: expire expires: this.mintExpire
}; };
this.generatedTokens[visitorId] = newPoToken; this.generatedTokens[idToUse] = newPoToken;
log("New PO Token: " + newPoToken.tokenBase64); log("New PO Token: " + newPoToken.tokenBase64);
if(_settings?.notify_bg) if(_settings?.notify_bg)
bridge.toast("New Botguard Token: " + newPoToken?.tokenBase64?.substring(0, 10) + "..."); bridge.toast("New Botguard Token: " + (type ? "(" + type + ") " : "") + newPoToken?.tokenBase64?.substring(0, 10) + "...");
cb(poToken); return poToken;
}); }
catch(ex) {
log("Minting failed due to: " + ex);
}
} }
} }
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"repositoryUrl": "https://futo.org", "repositoryUrl": "https://futo.org",
"scriptUrl": "./YoutubeScript.js", "scriptUrl": "./YoutubeScript.js",
"version": 220, "version": 231,
"iconUrl": "./youtube.png", "iconUrl": "./youtube.png",
"id": "35ae969a-a7db-11ed-afa1-0242ac120003", "id": "35ae969a-a7db-11ed-afa1-0242ac120003",
...@@ -71,6 +71,14 @@ ...@@ -71,6 +71,14 @@
"type": "Boolean", "type": "Boolean",
"default": "false" "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", "variable": "allowAgeRestricted",
"name": "Allow Age Restricted", "name": "Allow Age Restricted",
...@@ -92,6 +100,13 @@ ...@@ -92,6 +100,13 @@
"type": "Boolean", "type": "Boolean",
"default": "false" "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", "variable": "showVerboseToasts",
"name": "Show Verbose Messages", "name": "Show Verbose Messages",
...@@ -203,6 +218,14 @@ ...@@ -203,6 +218,14 @@
"description": "These are settings not intended for most users, but may help development or power users.", "description": "These are settings not intended for most users, but may help development or power users.",
"type": "Header" "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", "variable": "notify_cipher",
"name": "Show Cipher every Video", "name": "Show Cipher every Video",
...@@ -216,6 +239,13 @@ ...@@ -216,6 +239,13 @@
"description": "Shows a toast with the botguard token used changed", "description": "Shows a toast with the botguard token used changed",
"type": "Boolean", "type": "Boolean",
"default": "false" "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"
} }
], ],
......