diff --git a/arc-sw.js b/arc-sw.js new file mode 100644 index 0000000..19b51e3 --- /dev/null +++ b/arc-sw.js @@ -0,0 +1 @@ +!function(t){var e={};function r(n){if(e[n])return e[n].exports;var o=e[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)r.d(n,o,function(e){return t[e]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=100)}({100:function(t,e,r){"use strict";r.r(e);var n=r(2);if("undefined"!=typeof ServiceWorkerGlobalScope){var o="https://arc.io"+n.k;importScripts(o)}else if("undefined"!=typeof SharedWorkerGlobalScope){var c="https://arc.io"+n.i;importScripts(c)}else if("undefined"!=typeof DedicatedWorkerGlobalScope){var i="https://arc.io"+n.b;importScripts(i)}},2:function(t,e,r){"use strict";r.d(e,"a",(function(){return n})),r.d(e,"f",(function(){return c})),r.d(e,"j",(function(){return i})),r.d(e,"i",(function(){return a})),r.d(e,"b",(function(){return d})),r.d(e,"k",(function(){return f})),r.d(e,"c",(function(){return u})),r.d(e,"d",(function(){return s})),r.d(e,"e",(function(){return l})),r.d(e,"h",(function(){return m})),r.d(e,"g",(function(){return v}));var n={images:["bmp","jpeg","jpg","ttf","pict","svg","webp","eps","svgz","gif","png","ico","tif","tiff","bpg","avif","jxl"],video:["mp4","3gp","webm","mkv","flv","f4v","f4p","f4bogv","drc","avi","mov","qt","wmv","amv","mpg","mp2","mpeg","mpe","m2v","m4v","3g2","gifv","mpv","av1"],audio:["mid","midi","aac","aiff","flac","m4a","m4p","mp3","ogg","oga","mogg","opus","ra","rm","wav","webm","f4a","pat"],interchange:["json","yaml","xml","csv","toml","ini","bson","asn1","ubj"],archives:["jar","iso","tar","tgz","tbz2","tlz","gz","bz2","xz","lz","z","7z","apk","dmg","rar","lzma","txz","zip","zipx"],documents:["pdf","ps","doc","docx","ppt","pptx","xls","otf","xlsx"],other:["srt","swf"]},o="arc:",c={COMLINK_INIT:"".concat(o,"comlink:init"),NODE_ID:"".concat(o,":nodeId"),CDN_CONFIG:"".concat(o,"cdn:config"),P2P_CLIENT_READY:"".concat(o,"cdn:ready"),STORED_FIDS:"".concat(o,"cdn:storedFids"),SW_HEALTH_CHECK:"".concat(o,"cdn:healthCheck"),WIDGET_CONFIG:"".concat(o,"widget:config"),WIDGET_INIT:"".concat(o,"widget:init"),WIDGET_UI_LOAD:"".concat(o,"widget:load"),BROKER_LOAD:"".concat(o,"broker:load"),RENDER_FILE:"".concat(o,"inlay:renderFile"),FILE_RENDERED:"".concat(o,"inlay:fileRendered")},i="serviceWorker",a="/".concat("shared-worker",".js"),d="/".concat("dedicated-worker",".js"),f="/".concat("arc-sw-core",".js"),p="".concat("arc-sw",".js"),u=("/".concat(p),"/".concat("arc-sw"),"arc-db"),s="key-val-store",l=2**17,m="".concat("https://overmind.arc.io","/api/propertySession"),v="".concat("https://warden.arc.io","/mailbox/propertySession")}}); \ No newline at end of file diff --git a/index.html b/index.html index 87bfffd..c3ddfb2 100755 --- a/index.html +++ b/index.html @@ -94,6 +94,7 @@ text-align:center; +

Contact me

diff --git a/info.js b/info.js deleted file mode 100644 index f361f84..0000000 --- a/info.js +++ /dev/null @@ -1,482 +0,0 @@ -const querystring = require('querystring'); -const sax = require('sax'); -const miniget = require('miniget'); -const utils = require('./utils'); -// Forces Node JS version of setTimeout for Electron based applications -const { setTimeout } = require('timers'); -const formatUtils = require('./format-utils'); -const urlUtils = require('./url-utils'); -const extras = require('./info-extras'); -const sig = require('./sig'); -const Cache = require('./cache'); - - -const BASE_URL = 'https://www.youtube.com/watch?v='; - - -// Cached for storing basic/full info. -exports.cache = new Cache(); -exports.cookieCache = new Cache(1000 * 60 * 60 * 24); -exports.watchPageCache = new Cache(); - - -// Special error class used to determine if an error is unrecoverable, -// as in, ytdl-core should not try again to fetch the video metadata. -// In this case, the video is usually unavailable in some way. -class UnrecoverableError extends Error {} - - -// List of URLs that show up in `notice_url` for age restricted videos. -const AGE_RESTRICTED_URLS = [ - 'support.google.com/youtube/?p=age_restrictions', - 'youtube.com/t/community_guidelines', -]; - - -/** - * Gets info from a video without getting additional formats. - * - * @param {string} id - * @param {Object} options - * @returns {Promise} -*/ -exports.getBasicInfo = async(id, options) => { - const retryOptions = Object.assign({}, miniget.defaultOptions, options.requestOptions); - options.requestOptions = Object.assign({}, options.requestOptions, {}); - options.requestOptions.headers = Object.assign({}, - { - // eslint-disable-next-line max-len - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36', - }, options.requestOptions.headers); - const validate = info => { - let playErr = utils.playError(info.player_response, ['ERROR'], UnrecoverableError); - let privateErr = privateVideoError(info.player_response); - if (playErr || privateErr) { - throw playErr || privateErr; - } - return info && info.player_response && ( - info.player_response.streamingData || isRental(info.player_response) || isNotYetBroadcasted(info.player_response) - ); - }; - let info = await pipeline([id, options], validate, retryOptions, [ - getWatchHTMLPage, - //getWatchJSONPage, - //getVideoInfoPage, - ]); - - Object.assign(info, { - formats: parseFormats(info.player_response), - related_videos: extras.getRelatedVideos(info), - }); - - // Add additional properties to info. - const media = extras.getMedia(info); - const additional = { - author: extras.getAuthor(info), - media, - likes: extras.getLikes(info), - dislikes: extras.getDislikes(info), - age_restricted: !!(media && media.notice_url && AGE_RESTRICTED_URLS.some(url => media.notice_url.includes(url))), - - // Give the standard link to the video. - video_url: BASE_URL + id, - storyboards: extras.getStoryboards(info), - chapters: extras.getChapters(info), - }; - - info.videoDetails = extras.cleanVideoDetails(Object.assign({}, - info.player_response && info.player_response.microformat && - info.player_response.microformat.playerMicroformatRenderer, - info.player_response && info.player_response.videoDetails, additional), info); - - return info; -}; - -const privateVideoError = player_response => { - let playability = player_response && player_response.playabilityStatus; - if (playability && playability.status === 'LOGIN_REQUIRED' && playability.messages && - playability.messages.filter(m => /This is a private video/.test(m)).length) { - return new UnrecoverableError(playability.reason || (playability.messages && playability.messages[0])); - } else { - return null; - } -}; - - -const isRental = player_response => { - let playability = player_response.playabilityStatus; - return playability && playability.status === 'UNPLAYABLE' && - playability.errorScreen && playability.errorScreen.playerLegacyDesktopYpcOfferRenderer; -}; - - -const isNotYetBroadcasted = player_response => { - let playability = player_response.playabilityStatus; - return playability && playability.status === 'LIVE_STREAM_OFFLINE'; -}; - - -const getWatchHTMLURL = (id, options) => `${BASE_URL + id}&hl=${options.lang || 'en'}`; -const getWatchHTMLPageBody = (id, options) => { - const url = getWatchHTMLURL(id, options); - return exports.watchPageCache.getOrSet(url, () => utils.exposedMiniget(url, options).text()); -}; - - -const EMBED_URL = 'https://www.youtube.com/embed/'; -const getEmbedPageBody = (id, options) => { - const embedUrl = `${EMBED_URL + id}?hl=${options.lang || 'en'}`; - return utils.exposedMiniget(embedUrl, options).text(); -}; - - -const getHTML5player = body => { - let html5playerRes = - /|"jsUrl":"([^"]+)"/ - .exec(body); - return html5playerRes ? html5playerRes[1] || html5playerRes[2] : null; -}; - - -const getIdentityToken = (id, options, key, throwIfNotFound) => - exports.cookieCache.getOrSet(key, async() => { - let page = await getWatchHTMLPageBody(id, options); - let match = page.match(/(["'])ID_TOKEN\1[:,]\s?"([^"]+)"/); - if (!match && throwIfNotFound) { - throw new UnrecoverableError('Cookie header used in request, but unable to find YouTube identity token'); - } - return match && match[2]; - }); - - -/** - * Goes through each endpoint in the pipeline, retrying on failure if the error is recoverable. - * If unable to succeed with one endpoint, moves onto the next one. - * - * @param {Array.} args - * @param {Function} validate - * @param {Object} retryOptions - * @param {Array.} endpoints - * @returns {[Object, Object, Object]} - */ -const pipeline = async(args, validate, retryOptions, endpoints) => { - let info; - for (let func of endpoints) { - try { - const newInfo = await retryFunc(func, args.concat([info]), retryOptions); - if (newInfo.player_response) { - newInfo.player_response.videoDetails = assign( - info && info.player_response && info.player_response.videoDetails, - newInfo.player_response.videoDetails); - newInfo.player_response = assign(info && info.player_response, newInfo.player_response); - } - info = assign(info, newInfo); - if (validate(info, false)) { - break; - } - } catch (err) { - if (err instanceof UnrecoverableError || func === endpoints[endpoints.length - 1]) { - throw err; - } - // Unable to find video metadata... so try next endpoint. - } - } - return info; -}; - - -/** - * Like Object.assign(), but ignores `null` and `undefined` from `source`. - * - * @param {Object} target - * @param {Object} source - * @returns {Object} - */ -const assign = (target, source) => { - if (!target || !source) { return target || source; } - for (let [key, value] of Object.entries(source)) { - if (value !== null && value !== undefined) { - target[key] = value; - } - } - return target; -}; - - -/** - * Given a function, calls it with `args` until it's successful, - * or until it encounters an unrecoverable error. - * Currently, any error from miniget is considered unrecoverable. Errors such as - * too many redirects, invalid URL, status code 404, status code 502. - * - * @param {Function} func - * @param {Array.} args - * @param {Object} options - * @param {number} options.maxRetries - * @param {Object} options.backoff - * @param {number} options.backoff.inc - */ -const retryFunc = async(func, args, options) => { - let currentTry = 0, result; - while (currentTry <= options.maxRetries) { - try { - result = await func(...args); - break; - } catch (err) { - if (err instanceof UnrecoverableError || - (err instanceof miniget.MinigetError && err.statusCode < 500) || currentTry >= options.maxRetries) { - throw err; - } - let wait = Math.min(++currentTry * options.backoff.inc, options.backoff.max); - await new Promise(resolve => setTimeout(resolve, wait)); - } - } - return result; -}; - - -const jsonClosingChars = /^[)\]}'\s]+/; -const parseJSON = (source, varName, json) => { - if (!json || typeof json === 'object') { - return json; - } else { - try { - json = json.replace(jsonClosingChars, ''); - return JSON.parse(json); - } catch (err) { - throw Error(`Error parsing ${varName} in ${source}: ${err.message}`); - } - } -}; - - -const findJSON = (source, varName, body, left, right, prependJSON) => { - let jsonStr = body.split("ytInitialPlayerResponse = {")[1].split("}}};")[0] += "}}}"; - if (!jsonStr) { - throw Error(`Could not find ${varName} in ${source}`); - } - return parseJSON(source, varName, utils.cutAfterJSON(`${prependJSON}${jsonStr}`)); -}; - - -const findPlayerResponse = (source, info) => { - const player_response = info && ( - (info.args && info.args.player_response) || - info.player_response || info.playerResponse || info.embedded_player_response); - return parseJSON(source, 'player_response', player_response); -}; - - -const getWatchJSONURL = (id, options) => `${getWatchHTMLURL(id, options)}&pbj=1`; -const getWatchJSONPage = async(id, options) => { - const reqOptions = Object.assign({ headers: {} }, options.requestOptions); - let cookie = reqOptions.headers.Cookie || reqOptions.headers.cookie; - reqOptions.headers = Object.assign({ - 'x-youtube-client-name': '1', - 'x-youtube-client-version': '2.20201203.06.00', - 'x-youtube-identity-token': exports.cookieCache.get(cookie || 'browser') || '', - }, reqOptions.headers); - - const setIdentityToken = async(key, throwIfNotFound) => { - if (reqOptions.headers['x-youtube-identity-token']) { return; } - reqOptions.headers['x-youtube-identity-token'] = await getIdentityToken(id, options, key, throwIfNotFound); - }; - - if (cookie) { - await setIdentityToken(cookie, true); - } - - const jsonUrl = getWatchJSONURL(id, options); - const body = await utils.exposedMiniget(jsonUrl, options, reqOptions).text(); - let parsedBody = parseJSON('watch.json', 'body', body); - if (parsedBody.reload === 'now') { - await setIdentityToken('browser', false); - } - if (parsedBody.reload === 'now' || !Array.isArray(parsedBody)) { - throw Error('Unable to retrieve video metadata in watch.json'); - } - let info = parsedBody.reduce((part, curr) => Object.assign(curr, part), {}); - info.player_response = findPlayerResponse('watch.json', info); - info.html5player = info.player && info.player.assets && info.player.assets.js; - - return info; -}; - - -const getWatchHTMLPage = async(id, options) => { - let body = await getWatchHTMLPageBody(id, options); - let info = { page: 'watch' }; - try { - info.player_response = findJSON('watch.html', 'player_response', - body, /\bytInitialPlayerResponse\s*=\s*\{/i, '\n', '{'); - } catch (err) { - let args = findJSON('watch.html', 'player_response', body, /\bytplayer\.config\s*=\s*{/, '', '{'); - info.player_response = findPlayerResponse('watch.html', args); - } - info.response = findJSON('watch.html', 'response', body, /\bytInitialData("\])?\s*=\s*\{/i, '\n', '{'); - info.html5player = getHTML5player(body); - return info; -}; - - -const INFO_HOST = 'www.youtube.com'; -const INFO_PATH = '/get_video_info'; -const VIDEO_EURL = 'https://youtube.googleapis.com/v/'; -const getVideoInfoPage = async(id, options) => { - const url = new URL(`https://${INFO_HOST}${INFO_PATH}`); - url.searchParams.set('video_id', id); - url.searchParams.set('eurl', VIDEO_EURL + id); - url.searchParams.set('ps', 'default'); - url.searchParams.set('gl', 'US'); - url.searchParams.set('hl', options.lang || 'en'); - url.searchParams.set('html5', '1'); - const body = await utils.exposedMiniget(url.toString(), options).text(); - let info = querystring.parse(body); - info.player_response = findPlayerResponse('get_video_info', info); - return info; -}; - - -/** - * @param {Object} player_response - * @returns {Array.} - */ -const parseFormats = player_response => { - let formats = []; - if (player_response && player_response.streamingData) { - formats = formats - .concat(player_response.streamingData.formats || []) - .concat(player_response.streamingData.adaptiveFormats || []); - } - return formats; -}; - - -/** - * Gets info from a video additional formats and deciphered URLs. - * - * @param {string} id - * @param {Object} options - * @returns {Promise} - */ -exports.getInfo = async(id, options) => { - let info = await exports.getBasicInfo(id, options); - const hasManifest = - info.player_response && info.player_response.streamingData && ( - info.player_response.streamingData.dashManifestUrl || - info.player_response.streamingData.hlsManifestUrl - ); - let funcs = []; - if (info.formats.length) { - info.html5player = info.html5player || - getHTML5player(await getWatchHTMLPageBody(id, options)) || getHTML5player(await getEmbedPageBody(id, options)); - if (!info.html5player) { - throw Error('Unable to find html5player file'); - } - const html5player = new URL(info.html5player, BASE_URL).toString(); - funcs.push(sig.decipherFormats(info.formats, html5player, options)); - } - if (hasManifest && info.player_response.streamingData.dashManifestUrl) { - let url = info.player_response.streamingData.dashManifestUrl; - funcs.push(getDashManifest(url, options)); - } - if (hasManifest && info.player_response.streamingData.hlsManifestUrl) { - let url = info.player_response.streamingData.hlsManifestUrl; - funcs.push(getM3U8(url, options)); - } - - let results = await Promise.all(funcs); - info.formats = Object.values(Object.assign({}, ...results)); - info.formats = info.formats.map(formatUtils.addFormatMeta); - info.formats.sort(formatUtils.sortFormats); - info.full = true; - return info; -}; - - -/** - * Gets additional DASH formats. - * - * @param {string} url - * @param {Object} options - * @returns {Promise>} - */ -const getDashManifest = (url, options) => new Promise((resolve, reject) => { - let formats = {}; - const parser = sax.parser(false); - parser.onerror = reject; - let adaptationSet; - parser.onopentag = node => { - if (node.name === 'ADAPTATIONSET') { - adaptationSet = node.attributes; - } else if (node.name === 'REPRESENTATION') { - const itag = parseInt(node.attributes.ID); - if (!isNaN(itag)) { - formats[url] = Object.assign({ - itag, url, - bitrate: parseInt(node.attributes.BANDWIDTH), - mimeType: `${adaptationSet.MIMETYPE}; codecs="${node.attributes.CODECS}"`, - }, node.attributes.HEIGHT ? { - width: parseInt(node.attributes.WIDTH), - height: parseInt(node.attributes.HEIGHT), - fps: parseInt(node.attributes.FRAMERATE), - } : { - audioSampleRate: node.attributes.AUDIOSAMPLINGRATE, - }); - } - } - }; - parser.onend = () => { resolve(formats); }; - const req = utils.exposedMiniget(new URL(url, BASE_URL).toString(), options); - req.setEncoding('utf8'); - req.on('error', reject); - req.on('data', chunk => { parser.write(chunk); }); - req.on('end', parser.close.bind(parser)); -}); - - -/** - * Gets additional formats. - * - * @param {string} url - * @param {Object} options - * @returns {Promise>} - */ -const getM3U8 = async(url, options) => { - url = new URL(url, BASE_URL); - const body = await utils.exposedMiniget(url.toString(), options).text(); - let formats = {}; - body - .split('\n') - .filter(line => /^https?:\/\//.test(line)) - .forEach(line => { - const itag = parseInt(line.match(/\/itag\/(\d+)\//)[1]); - formats[line] = { itag, url: line }; - }); - return formats; -}; - - -// Cache get info functions. -// In case a user wants to get a video's info before downloading. -for (let funcName of ['getBasicInfo', 'getInfo']) { - /** - * @param {string} link - * @param {Object} options - * @returns {Promise} - */ - const func = exports[funcName]; - exports[funcName] = async(link, options = {}) => { - utils.checkForUpdates(); - let id = await urlUtils.getVideoID(link); - const key = [funcName, id, options.lang].join('-'); - return exports.cache.getOrSet(key, () => func(id, options)); - }; -} - - -// Export a few helpers. -exports.validateID = urlUtils.validateID; -exports.validateURL = urlUtils.validateURL; -exports.getURLVideoID = urlUtils.getURLVideoID; -exports.getVideoID = urlUtils.getVideoID; diff --git a/whitefile.html b/whitefile.html index 28667ba..4330295 100644 --- a/whitefile.html +++ b/whitefile.html @@ -16,5 +16,6 @@ + \ No newline at end of file