script
: let fm = FileManager.local();
let save, pref, path = fm.joinPath(fm.documentsDirectory(), '[mainDict.filename]');
if (!(save = !fm.fileExists(path))) {
pref = JSON.parse(fm.readString(path));
let wait;
if (wait = pref.wait) {
delete pref.wait;
fm.writeString(path, JSON.stringify(pref));
let delay;
if ((delay = Math.trunc((new Date(wait).getTime() - Date.now())/1000)) > 0) return {wait:delay};
}
save = pref?.prefversion >= 4 && pref?.prefversion <= 6;
}
let [o1, o2] = await Promise.all([ytcheck(), updatecheck()]);
let o = {...o1, ...o2};
if (!o.post && !o.live) { o.count = o1.output?.length; }
return o;
async function updatecheck() {
if (save) { pref = [mainDict.defaultpref]; }
let mainv = parseInt([mainDict.prefversion]);
if (pref.prefversion > mainv) { return {old:1}; }
if (pref.prefversion < mainv) {
save = true;
pref.prefversion = mainv;
}
let out = {pref:pref};
if (Date.now() > (pref.updatecheck ?? 0) + 79200000) { // 22 hr
try {
let r = new Request('https://ytupdate.gluebyte.workers.dev/?id=[mainDict.id]');
r.timeoutInterval = 3;
let p = await r.loadString();
if (p.startsWith('\n2') && p.substring(1, 11) !== '[mainDict.version]') { out.version = p; }
pref.updatecheck = Date.now();
save = true;
} catch {}
}
if (save) try { fm.writeString(path, JSON.stringify(pref)) } catch { return {init:1}; }
return out;
}
async function ytcheck() {
let input = args.shortcutParameter.replace('youtu.be/', 'youtube.com/watch?v=');
let out = await ytinfo(input);
return out.error ? out : {output:[out], post:out.post, live:out.live, user:out.account};
}
async function ytinfo(input) {
let getURL = a => {
let m, d = {url: a.url};
a.signatureCipher?.split('&').forEach(b=>{ d[(c=b.split('='))[0]] = decodeURIComponent(c[1]) });
if (m=d.url.match(/&n=([^&]+)/)) { d.url = d.url.replace('&n='+m[1], '&n='+nfunc(m[1])); }
return d.url + (d.sp ? '&'+ d.sp +'='+ encodeURIComponent(decipher(d.s)) : '');
}
let out = {url:input};
let r = new Request(input);
r.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36'};
let p = await r.loadString();
if (p.includes('="https://policies.google.com/technologies/cookies')) {
let rr = new Request('https://consent.youtube.com/save');
rr.method = 'POST';
rr.headers = {'content-type':'application/x-www-form-urlencoded'};
rr.body = 'set_ytc=true&set_apyt=true&set_eom=false';
await rr.load();
let w = new WebView();
await w.loadURL(input);
p = await w.getHTML();
}
if (p.includes('="https://policies.google.com/technologies/cookies')) { return {cookie:1}; }
{let m = p.match(/"USER_ACCOUNT_NAME":"([^"]+)/);
if (m) {
out.account = "[mainDict.msg.userprompt]" + m[1];
}}
if (input.includes('youtube.com/post/')) {
let d = JSON.parse(p.match(/ytInitialData = (\{.+\});<\/script>/)?.[1] || JSON.parse('{"a":"' + p.match(/ytInitialData = '([^']+)/)[1].replace(/\\x/g,'\\u00') + '"}').a);
let post = d.contents.twoColumnBrowseResultsRenderer || d.contents.singleColumnBrowseResultsRenderer;
post = post.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].backstagePostThreadRenderer.post.backstagePostRenderer;
let name = post.authorText.runs[0].text.replace(/^\./,'\u200b.') + ' - ' + post.contentText.runs[0].text.replace(/\n+$/,'').replace(/\n+/g,' ');
let urls = (post?.backstageAttachment?.postMultiImageRenderer?.images || [post.backstageAttachment]).map(a=>a.backstageImageRenderer?.image.thumbnails.reduce((b,c)=>c.width>b[1]?[c.url.replace(/-rwa$/,''),c.width]:b,['',0])[0]).filter(Boolean);
return {...out, post:true, name:name, urls:urls};
}
let auth, vdt = p.match(/"VISITOR_DATA":"([^"]+)/)[1];
{let sid = p.match(/"USER_SESSION_ID":"([^"]+)/)?.[1];
if (sid) {
let date = Math.round(Date.now()/1000).toString();
sid += ' ' + date + ' ';
let ck = {};
r.response.cookies.forEach(a => ck[a.name] = a.value);
let sapis3 = ck['__Secure-3PAPISID'];
auth = 'SAPISIDHASH ' + date + '_' + sha1(sid + (ck['SAPISID'] || sapis3) + ' https://www.youtube.com') + '_u SAPISID1PHASH ' + date + '_' + sha1(sid + ck['__Secure-1PAPISID'] + ' https://www.youtube.com') + '_u SAPISID3PHASH ' + date + '_' + sha1(sid + sapis3 + ' https://www.youtube.com') + '_u';
}}
let id = input.match(/v=([^&\?]+)/)?.[1] || p.match(/video(?:I|_i)d\\?":\\?"([^"\\]+)/)[1];
{let date;
if (date = p.match(/"publishDate":"([^"]+)/)?.[1]) {
date = new Date(date);
date = new Date(date.getTime() - 60000 * date.getTimezoneOffset());
} else { date = Date.now(); }
out.date = ' -t ' + new Date(date).toISOString().replace(/\D/g,'').replace(/(..)...$/,'.$1');}
let js = p.match(/\/s\/player\/[^"]+base.js/)[0];
r = new Request('https://www.youtube.com'+js);
p = await r.loadString();
let sts = p.match(/signatureTimestamp:(\d+)/)[1];
try {
let mm = p.match(/'use strict';var ([\w$_]+)=(\[".*?"\]|["'].+?split\(.*?\))(?=,)/s) || ['', '[]', 'Yname'];
let Yvar, Yname = mm[1].replace(/\$/g,'\\$');
eval('Yvar='+mm[2]);
let Yjoin = Yvar.indexOf('join');
mm = p.match(new RegExp(`([$_\\w]+)\\(decodeURIComponent\\([^)]+\\)\\),\\w+(?:\\.set|\\[${Yname}\\[${Yvar.indexOf('set')}\\]\\])\\(\\w+,encodeURIComponent`))[1];
let ms = p.match(new RegExp(`${mm.replace(/\$/g,'\\$')}=function\\(\\w+\\).+?(?:join|\\[${Yname}\\[${Yjoin}\\]\\])\\((?:""|${Yname}\\[${Yvar.indexOf('')}\\])\\)};`));
let mh = p.match(new RegExp('var '+ms[0].match(/;([$_\w]+)[\.\[]/)[1].replace(/\$/g,'\\$')+'=\{[\\s\\S]+?\}\};'))[0];
mm = p.match(new RegExp('var '+p.match(/(?:get\((?:"n"|.)\)|\|\|null)\)&&\(.=([$_\w]+)/)[1].replace(/\$/g,'\\$')+'=\\[(\[$_\\w\]+)'))[1];
let mn = p.match(new RegExp(`${mm.replace(/\$/g,'\\$')}=(function\\(.\\)\\{.+?(?:\\.join|\\[${Yname}\\[${Yjoin}\\]\\])\\(.*?\\)\\};)`,'s'))[1].replace(/;if\(typeof [$_\w]+===[^)]*\)return [$_\w]+;/g, ';');
let rep = s => s.replace(new RegExp(`\\b${Yname}\\[(\\d+)\\]`, 'g'), (_, a) => JSON.stringify({_:Yvar[a]}).replace(/^\{"_":|\}$/g,''));
eval(rep(mh) + 'decipher=' + rep(ms[0]) + 'nfunc=' + rep(mn));
} catch {
return {error:'js ' + js.match(/\/player\/([^\/]+)/)?.[1]};
}
let headers = {'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36', Origin: 'https://www.youtube.com', 'X-Goog-Visitor-Id': vdt};
r = new Request('https://www.youtube.com/youtubei/v1/player');
r.method = 'POST';
r.headers = auth ? {...headers, Authorization: auth} : headers;
r.body = JSON.stringify({"context": {"client": {"clientName": "TVHTML5", "clientVersion": "7.20250312.16.00"}}, "contentCheckOk": true, "racyCheckOk": true, "playbackContext": {"contentPlaybackContext": {"signatureTimestamp": sts}}, "videoId": id});
let r2 = new Request('https://www.youtube.com/youtubei/v1/player');
r2.method = 'POST';
r2.headers = headers;
r2.body = JSON.stringify({'context': {'client': {'clientName': 'IOS', 'clientVersion': '20.10.4', 'deviceModel': 'iPhone16,2'}}, "videoId": id});
let r3 = new Request('https://www.youtube.com/youtubei/v1/player');
r3.method = 'POST';
r3.headers = auth ? {...headers, Authorization: auth} : headers;
r3.body = JSON.stringify({'context': {'client': {'clientName': input.includes('music.youtube')?'WEB_REMIX':'WEB', 'clientVersion': input.includes('music.youtube')?'1.20250310.01.00':'2.20250312.04.00'}}, "videoId": id});
let [p1, p2, p3] = await Promise.all([r.loadJSON(), r2.loadJSON(), r3.loadJSON()]);
if (p1.playabilityStatus?.status !== 'OK') {
delete p1.playabilityStatus?.skip;
delete p1.playabilityStatus?.messages;
delete p1.playabilityStatus?.miniplayer;
delete p1.playabilityStatus?.errorScreen;
delete p1.playabilityStatus?.contextParams;
delete p1.playabilityStatus?.playableInEmbed;
delete p1.playabilityStatus?.pictureInPicture;
delete p1.playabilityStatus?.liveStreamability;
return {error:JSON.stringify(p1.playabilityStatus??'Status Error',null,4), login:(auth?undefined:1)};
}
out.pip = p1.streamingData?.hlsManifestUrl || p2.streamingData?.hlsManifestUrl;
let d = p1.videoDetails;
let duration = d.lengthSeconds;
out.duration = Math.floor(duration/3600).toString().padStart(2,'0')+':'+Math.floor((duration%3600)/60).toString().padStart(2,'0')+':'+(duration%60).toString().padStart(2,'0');
out.user = p3.videoDetails.author;
out.title = p3.videoDetails.title.replace(/^\./,'\u200b.');
out.id3 = `-metadata artist="${out.user.replace(/[\u0022'\\]/g,'\\$0')}" -metadata title="${out.title.replace(/[\u0022'\\]/g,'\\$0')}"`;
out.thumb = (d.thumbnail?.thumbnails?.[0]?.url ?? '').match(/^[^?]+/)[0].replace(/\w*default\.jpg/,'maxresdefault.jpg').replace(/\w*(\d\.jpg)/,'oar$1');
if (d.isPostLiveDvr) { return {...out, live:true} }
if (d.isLive && out.pip) {
r = new Request(out.pip);
p = await r.loadString();
let audio, m, quality = [];
if (m = p.match(/^#EXT-X-MEDIA:.*/gm).filter(a=>a.includes('TYPE=AUDIO'))) {
audio = m[m.length-1].match(/http[^"]*/);
}
m = p.match(/^#EXT-X-STREAM-INF.*\nhttp.*/gm) ?? [];
for (let v of m) {
let res ='mp4\u3000' + v.match(/RESOLUTION=(\w+)/)[1];
quality.push(res);
out[res] = v.match(/\n(.+)/)[1] + (audio ? (' -i ' + audio) : '');
}
out.quality = quality;
out.video = true;
out.live = true;
return out;
} else {
let video = {};
let audio = {};
p1.streamingData.adaptiveFormats.forEach(a => {
if (a.mimeType.startsWith('audio')) {
let ext = a.mimeType.match(/audio\/(\w+)/)[1];
if (ext === 'mp4') { ext = 'm4a' }
if (audio[ext]?.bitrate ?? 0 < a.bitrate) {
let size = Math.round(parseInt(a.contentLength||0)/1000) + 'KB';
audio[ext] = {curl:getURL(a), bytes:a.contentLength||0, curlext:'audio', ext:ext, size:size, donuts:parseInt('0'+(a.contentLength||'').slice(0,-7))+1};
}
}
});
p1.streamingData.adaptiveFormats.forEach(a => {
if (a.mimeType.startsWith('video')) {
let ext = a.mimeType.match(/video\/(\w+)/)[1];
let codec = ext==='mp4' ? ' (' + a.mimeType.match(/codecs..(\w+)/)?.[1].replace('avc1','AVC').replace('av01','AV1') + ')' : '';
let quality = a.qualityLabel + ' ' + ext + codec + (codec.includes('AVC') ? ' 🖼️' : [26 codec.includes('AV1') ? ' 📁' : ]'');
if (video[quality]?.bitrate ?? 0 < a.bitrate) {
let size = Math.round((parseInt(a.contentLength||0) + parseInt((audio[ext==='mp4'?'m4a':'webm'] || audio.m4a).bytes))/1000000) + 'MB';
video[quality] = {curl:getURL(a), bytes:a.contentLength||0, curlext:'video', ext:ext, size:size, donuts:parseInt('0'+(a.contentLength||'').slice(0,-7))+1};
}
}
});
out.video = Object.fromEntries(Object.entries(video).map(([k,v]) => [k+'\u3000'+v.size, v]));
out.audio = audio;
}
let caption = {};
let translationlang = [];
p1.captions?.playerCaptionsTracklistRenderer?.captionTracks.forEach(a => {
caption[a.name.simpleText] = {url:a.baseUrl.replace(/&fmt=\w+/, '')+'&fmt=vtt', lang:a.languageCode};
// if (a.isTranslatable) { translationlang.push(a.name.runs[0].text); }
})
if (Object.keys(caption).length > 0) {
out.caption = caption;
/*if (t = p1.captions.playerCaptionsTracklistRenderer.translationLanguages) {
let translation = {}
t.forEach(a => translation[a.languageName.runs[0].text] = a.languageCode)
out.translation = translation
} else {
out.translation = {"Afrikaans":"af","Akan":"ak","Albanian":"sq","Amharic":"am","Arabic":"ar","Armenian":"hy","Assamese":"as","Aymara":"ay","Azerbaijani":"az","Bangla":"bn","Basque":"eu","Belarusian":"be","Bhojpuri":"bho","Bosnian":"bs","Bulgarian":"bg","Burmese":"my","Catalan":"ca","Cebuano":"ceb","Chinese (Simplified)":"zh-Hans","Chinese (Traditional)":"zh-Hant","Corsican":"co","Croatian":"hr","Czech":"cs","Danish":"da","Divehi":"dv","Dutch":"nl","English":"en","Esperanto":"eo","Estonian":"et","Ewe":"ee","Filipino":"fil","Finnish":"fi","French":"fr","Galician":"gl","Ganda":"lg","Georgian":"ka","German":"de","Greek":"el","Guarani":"gn","Gujarati":"gu","Haitian Creole":"ht","Hausa":"ha","Hawaiian":"haw","Hebrew":"iw","Hindi":"hi","Hmong":"hmn","Hungarian":"hu","Icelandic":"is","Igbo":"ig","Indonesian":"id","Irish":"ga","Italian":"it","Japanese":"ja","Javanese":"jv","Kannada":"kn","Kazakh":"kk","Khmer":"km","Kinyarwanda":"rw","Korean":"ko","Krio":"kri","Kurdish":"ku","Kyrgyz":"ky","Lao":"lo","Latin":"la","Latvian":"lv","Lingala":"ln","Lithuanian":"lt","Luxembourgish":"lb","Macedonian":"mk","Malagasy":"mg","Malay":"ms","Malayalam":"ml","Maltese":"mt","Māori":"mi","Marathi":"mr","Mongolian":"mn","Nepali":"ne","Northern Sotho":"nso","Norwegian":"no","Nyanja":"ny","Odia":"or","Oromo":"om","Pashto":"ps","Persian":"fa","Polish":"pl","Portuguese":"pt","Punjabi":"pa","Quechua":"qu","Romanian":"ro","Russian":"ru","Samoan":"sm","Sanskrit":"sa","Scottish Gaelic":"gd","Serbian":"sr","Shona":"sn","Sindhi":"sd","Sinhala":"si","Slovak":"sk","Slovenian":"sl","Somali":"so","Southern Sotho":"st","Spanish":"es","Sundanese":"su","Swahili":"sw","Swedish":"sv","Tajik":"tg","Tamil":"ta","Tatar":"tt","Telugu":"te","Thai":"th","Tigrinya":"ti","Tsonga":"ts","Turkish":"tr","Turkmen":"tk","Ukrainian":"uk","Urdu":"ur","Uyghur":"ug","Uzbek":"uz","Vietnamese":"vi","Welsh":"cy","Western Frisian":"fy","Xhosa":"xh","Yiddish":"yi","Yoruba":"yo","Zulu":"zu"}
}
out.translationlang = translationlang.sort()*/
}
return {[out.title]:out, account:out.account};
}
function sha1 (str) {
// discuss at: https://locutus.io/php/sha1/
// original by: Webtoolkit.info (https://www.webtoolkit.info/)
// improved by: Michael White (https://getsprink.com)
// improved by: Kevin van Zonneveld (https://kvz.io)
// input by: Brett Zamir (https://brett-zamir.me)
// note 1: Keep in mind that in accordance with PHP, the whole string is buffered and then
// note 1: hashed. If available, we'd recommend using Node's native crypto modules directly
// note 1: in a steaming fashion for faster and more efficient hashing
// example 1: sha1('Kevin van Zonneveld')
// returns 1: '54916d2e62f65b3afa6e192e6a601cdbe5cb5897'
// modified by: gluebyte to un-utf8
var hash; try { var crypto = require('crypto'); var sha1sum = crypto.createHash('sha1'); sha1sum.update(str); hash = sha1sum.digest('hex'); } catch (e) { hash = undefined; } if (hash !== undefined) { return hash; } var _rotLeft = function (n, s) { var t4 = (n << s) | (n >>> (32 - s)); return t4; }; var _cvtHex = function (val) { var str = ''; var i; var v; for (i = 7; i >= 0; i--) { v = (val >>> (i * 4)) & 0x0f; str += v.toString(16); } return str; }; var blockstart; var i, j; var W = new Array(80); var H0 = 0x67452301; var H1 = 0xEFCDAB89; var H2 = 0x98BADCFE; var H3 = 0x10325476; var H4 = 0xC3D2E1F0; var A, B, C, D, E; var temp;
// utf8_encode
// str = unescape(encodeURIComponent(str))
var strLen = str.length; var wordArray = []; for (i = 0; i < strLen - 3; i += 4) { j = str.charCodeAt(i) << 24 | str.charCodeAt(i + 1) << 16 | str.charCodeAt(i + 2) << 8 | str.charCodeAt(i + 3); wordArray.push(j); } switch (strLen % 4) { case 0: i = 0x080000000; break; case 1: i = str.charCodeAt(strLen - 1) << 24 | 0x0800000; break; case 2: i = str.charCodeAt(strLen - 2) << 24 | str.charCodeAt(strLen - 1) << 16 | 0x08000; break; case 3: i = str.charCodeAt(strLen - 3) << 24 | str.charCodeAt(strLen - 2) << 16 | str.charCodeAt(strLen - 1) << 8 | 0x80; break; } wordArray.push(i); while ((wordArray.length % 16) !== 14) { wordArray.push(0); } wordArray.push(strLen >>> 29); wordArray.push((strLen << 3) & 0x0ffffffff); for (blockstart = 0; blockstart < wordArray.length; blockstart += 16) { for (i = 0; i < 16; i++) { W[i] = wordArray[blockstart + i]; } for (i = 16; i <= 79; i++) { W[i] = _rotLeft(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1); } A = H0; B = H1; C = H2; D = H3; E = H4; for (i = 0; i <= 19; i++) { temp = (_rotLeft(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff; E = D; D = C; C = _rotLeft(B, 30); B = A; A = temp; } for (i = 20; i <= 39; i++) { temp = (_rotLeft(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff; E = D; D = C; C = _rotLeft(B, 30); B = A; A = temp; } for (i = 40; i <= 59; i++) { temp = (_rotLeft(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff; E = D; D = C; C = _rotLeft(B, 30); B = A; A = temp; } for (i = 60; i <= 79; i++) { temp = (_rotLeft(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff; E = D; D = C; C = _rotLeft(B, 30); B = A; A = temp; } H0 = (H0 + A) & 0x0ffffffff; H1 = (H1 + B) & 0x0ffffffff; H2 = (H2 + C) & 0x0ffffffff; H3 = (H3 + D) & 0x0ffffffff; H4 = (H4 + E) & 0x0ffffffff; } temp = _cvtHex(H0) + _cvtHex(H1) + _cvtHex(H2) + _cvtHex(H3) + _cvtHex(H4); return temp.toLowerCase();
}