//@ts-check import JadefinIntegrity from "./JadefinIntegrity.js"; import JadefinMod from "./JadefinMod.js"; import JadefinModules from "./JadefinModules.js"; import JadefinUtils from "./JadefinUtils.js"; export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefin"] = new (class Jadefin extends JadefinMod { modUrl = import.meta.url; root = this.modUrl.substring(0, this.modUrl.lastIndexOf("/")); /** @type {{[id: string]: JadefinMod}} */ mods = {}; /** @type {Set} */ _loadingMods_dedupe = new Set(); /** @type {{module: JadefinMod, name: String, url: String}[]} */ _loadingMods_all = []; /** @type {Promise} */ _loadingMods; /** @type {any} */ _webpackModuleFuncs; constructor() { super(); this.log.i(`Loading from ${this.modUrl}`); this.log.v("Test Verbose"); this.log.w("Test Warning"); this.log.e("Test Error"); this.initEarly(); JadefinModules.Jadefin = this; this._loadingMods = this._loadMods(); window["_JadefinLoaded"](); } initEarly() { // Required for mods to be able to perform certain worker shenanigans. window.Worker = (class Worker extends window.Worker { constructor(scriptURL, options) { const args = {scriptURL, options}; JadefinUtils.events.dispatchEvent(new CustomEvent(JadefinUtils.eventTypes.WORKER_CREATING, { detail: { args } })); super(args.scriptURL, args.options); JadefinUtils.events.dispatchEvent(new CustomEvent(JadefinUtils.eventTypes.WORKER_CREATED, { detail: { args, worker: this } })); } }); } async init(base) { const self = this; await super.init("Jadefin", this.modUrl); // Wait until webpackChunk and Emby exist. // It might be delayed due to the main jellyfin bundle being replaced by an inject script on Android. await JadefinUtils.waitUntil(() => window["webpackChunk"] && window["Emby"]); window["webpackChunk"].push([["custom_init"], {}, a => { /** @type {((id: number | string) => any & {[key: string]: any})} */ this.webpackLoad = a; }]); if (!this.webpackLoad) { this.log.e("Couldn't obtain webpackLoad"); return; } /** @type {(id: number | string) => any} */ this.webpackTryLoad = id => { const sid = `${id}`; const chunk = window["webpackChunk"].find(c => c[1][sid]); const chunkNames = chunk ? chunk[0].map(this.webpackChunkIdToJS) : null; if (chunkNames && (chunkNames.find(c => c.startsWith("session-login.") || c.startsWith("35463.") ))) { return null; } try { return this.webpackLoad?.(id); } catch (e) { this.log.w(`Failed to load webpack module ${id}`); this.log.dir(e); return null; } }; /** @type {(id: any) => string} */ this.webpackChunkIdToJS = Object.values(this.webpackLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`+".chunk.js"`) != -1); /** @type {(id: any) => string} */ this.webpackIdToCSS = Object.values(this.webpackLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`+".css"`) != -1); if (!this.webpackChunkIdToJS || !this.webpackIdToCSS) { this.log.e("Couldn't obtain webpackChunkIdToJS or webpackIdToCSS"); return; } /** @type {(name: any, cb: any, sid: any, id: any) => any} */ this.webpackLoadChunkLowLevel = Object.values(this.webpackLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`document.head.appendChild`) != -1); const webpackLoadChunkLowLevelKey = Object.keys(this.webpackLoad).find(k => this.webpackLoad?.[k] == this.webpackLoadChunkLowLevel); if (!this.webpackLoadChunkLowLevel || !webpackLoadChunkLowLevelKey) { this.log.e("Couldn't obtain webpackLoadChunkLowLevel"); return; } this.webpackLoad[webpackLoadChunkLowLevelKey] = function(name, cb, sid, id) { const _cb = cb; // self.log.v(`Webpack loading chunk ${id} (${sid}) from ${name}`); cb = (event) => { self.log.v(`Webpack loaded chunk ${id} (${sid}) from ${name}: ${event.type}`); return _cb(event); }; const rv = self.webpackLoadChunkLowLevel?.(name, cb, sid, id); return rv; }; /** @type {(id: any) => Promise} */ this.webpackLoadChunk = Object.values(this.webpackLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`Object.keys(`) != -1 && v.toString().indexOf(`}),[])`) != -1); // HACK: Load all chunks we could need when Jadefin initializes! // webpackChunkIdToJS contains all IDs either ahead of === or : // FIXME: Ideally, don't. This makes startup take ages and is unreliable! this.webpackChunkIds = [...this.webpackChunkIdToJS.toString().matchAll(/\d+(?=:|=)/g)].map(v => parseInt(v[0])); /** @type {any} */ this.webpackChunkJSs = {}; for (const id of this.webpackChunkIds) { this.webpackChunkJSs[this.webpackChunkIdToJS(id)] = id; } // Grabbed by running this on various screens: // (()=>{let a = webpackChunk.flatMap(c => c[0]); return JSON.stringify(a.filter((v,i)=>a.indexOf(v)==i))})() let webpackChunkIdsWanted = [ // Home JSON.parse("[1451,45642,23247,60815,81771,55931,1270,9886,12036,86897,59928,44965,56401,7495,17060,67224,67622,94048,36933,82363,36546,99377,82798,14577,78283,37658,94160,7184,83518,15277,71779,9911,82420,49398,32292,37821,74776,99994,87074,67942,60039,27017,65149,27182,4636,87530,4801,82255,99435,77077,71944,59874,21816,89409,79754,69285,43091,64380,40810,90186,87903,70118,64633,10905,68672,71318,86040,70555,41542,4836,30357,6270,49087,64706,19907,927,10672,2,56577,29593,55125,83354,49755,1680,39573,40394,60138,13151,65849,80835,14510,96307,16304,62155,84430,59258,39232,40465,18395,48979,68413,80183,85500,7011,22940,33067,15434,39435,78938,94047,55802,18084,79617,1998,15605,57949,21857,91737,28567,22424,44184,45568,5617,96084,40367,73233,52011,56422,27962,32762,35463,8372,28349,58782,29808,18119,24468,50777,84158,49275,35308,38965]"), // Login // JSON.parse("[1451,45642,23247,60815,81771,55931,1270,9886,12036,86897,59928,44965,56401,7495,17060,67224,67622,94048,36933,82363,36546,99377,82798,14577,69285,43091,64380,40810,90186,87903,70118,64633,10905,68672,71318,86040,70555,41542,4836,30357,6270,49087,64706,19907,927,10672,2,56577,29593,55125,83354,49755,1680,39573,40394,60138,13151,65849,80835,14510,96307,16304,62155,84430,59258,37658,94160,7184,83518,15277,71779,9911,82420,49398,32292,39232,40465,18395,48979,68413,60039,27017,65149,27182,4636,87530,4801,82255,99435,77077,71944,59874,21816,80183,85500,7011,22940,33067,15434,39435,78938,94047,55802,18084,79617,1998,15605,57949,21857,91737,28567,22424,44184,45568,5617,96084,40367,73233,52011,56422,27962,32762,35463,8372,28349,58782,29808,18119,24468,50777,84158,49275,35308,38965,11136,14044,16689,22943,27018,30010,39548,63365,94115,70594,98574,59015,58122,66935,28312,86355,88788,53147,89409,78283,37821,74776,99994,87074,67942,79754,1820,32125,28336,70712,93649,17244]"), ].flatMap(l => l); webpackChunkIdsWanted = webpackChunkIdsWanted.filter((id, i) => webpackChunkIdsWanted.indexOf(id) == i); // Grabbed through searching for module loads which still errored out. webpackChunkIdsWanted.push( ...Object.keys(this.webpackChunkJSs).filter(js => { return false || js.startsWith("user-display.") || js.startsWith("user-display-") || js.startsWith("53147.") || js.startsWith("activity.") || js.startsWith("node_modules.@juggle.") || false; }).map(js => this.webpackChunkJSs[js]) ); await Promise.all(webpackChunkIdsWanted.map(id => this.webpackLoadChunk?.(id))); // Wait until everything else is ready. await JadefinUtils.waitUntil(() => this.webpackModuleFuncs); // this._webpackUnsafeModuleIDs = this.findUnsafeWebpackModules(); const initing = [ this.initHookHtmlVideoPlayer(), ]; await this._loadingMods; await this.initMods(); await Promise.all(initing); this.log.i("Ready"); } async initHookHtmlVideoPlayer() { const self = this; await JadefinUtils.waitUntil(() => JadefinModules.htmlVideoPlayer); const _createMediaElement = JadefinModules.htmlVideoPlayer.prototype.createMediaElement; JadefinModules.htmlVideoPlayer.prototype.createMediaElement = function(options) { if (!JadefinUtils.htmlVideoPlayers.delete(this)) { self.log.i("Adding new htmlVideoPlayer"); self.log.dir(this); JadefinUtils.htmlVideoPlayers.add(this); JadefinUtils.htmlVideoPlayer = this; JadefinUtils.events.dispatchEvent(new CustomEvent(JadefinUtils.eventTypes.HTML_VIDEO_PLAYER_CHANGED, { detail: { alive: true, player: this } })); } return _createMediaElement.call(this, options); } const _destroy = JadefinModules.htmlVideoPlayer.prototype.destroy; JadefinModules.htmlVideoPlayer.prototype.destroy = function() { if (JadefinUtils.htmlVideoPlayers.delete(this)) { self.log.i("Removing old htmlVideoPlayer"); self.log.dir(this); JadefinUtils.events.dispatchEvent(new CustomEvent(JadefinUtils.eventTypes.HTML_VIDEO_PLAYER_CHANGED, { detail: { alive: false, player: this } })); } return _destroy.call(this); } } get webpackModuleFuncs() { return this._webpackModuleFuncs ??= this.webpackLoad ? Object.values(this.webpackLoad).find(v => typeof(v) == "object" && Object.keys(v).length > 10) : null; } /** * @param {(e: any) => any} cb */ findWebpackRawLoad(cb) { return Object.keys(this.webpackModuleFuncs).map(id => this.webpackTryLoad?.(id)).filter(e => e && cb(e)); } /** * @param {(e: any) => any} cb */ findWebpackModules(cb) { return Object.keys(this.webpackModuleFuncs).map(id => this.webpackTryLoad?.(id)?.default).filter(e => e && cb(e)); } /** * @param {(e: any) => any} cb */ findWebpackFunctions(cb) { return Object.keys(this.webpackModuleFuncs).map(id => this.webpackTryLoad?.(id)).filter(e => e && e instanceof Function && cb(e)); } /** * @param {any} [modules] */ findUnsafeWebpackModules(modules) { const unsafe = []; for (const id of Object.keys(modules || this.webpackModuleFuncs)) { try { this.webpackLoad?.(id); } catch (e) { this.log.w(`Failed to load webpack module ${id}`); this.log.w(this.webpackModuleFuncs[id].toString()); this.log.dir(e); unsafe.push(id); } } return unsafe; } /** * @param {string} name */ getMod(name) { let found = this.mods[name]; if (found) { return found; } for (let mod of Object.values(this.mods)) { if (mod.isMod(name)) { return mod; } } return undefined; } /** * @param {string[]} [list] */ async _loadMods(list) { if (!list) { // HACK: I'm hosting Jadefin on my own Jellyfin subdomain, but want my own mods... oops! let listUrl = window.location.host == "jellyfin.0x0a.de" ? `${this.root}/mods_jade.json` : `${this.root}/mods.json`; listUrl = localStorage.getItem("jadefin-mods") || listUrl; this.log.i(`Loading main mod list ${listUrl}`); list = window["JadefinModList"] || (await fetch(listUrl).then(r => r.json())); } if (!list) { return; } await Promise.all(list.map(async (/** @type {string} */ entry) => { let isList = false; if (entry.startsWith("list:")) { isList = true; entry = entry.substring("list:".length); } let name = entry; let url = `${this.root}/mods/${entry}`; if (entry.startsWith("//")) { url = window.location.protocol + entry; } else if (entry.startsWith("/")) { url = window.location.origin + entry; } else if (entry.indexOf(":") < entry.indexOf("?")) { url = entry; } let split = name.indexOf("?"); if (split != -1) { name = name.substring(0, split); } split = name.lastIndexOf("/"); if (split != -1) { name = name.substring(split + 1); } split = name.lastIndexOf("."); if (split != -1) { name = name.substring(0, split); } if (isList) { this.log.i(`Loading mod list ${url}`); const list = await fetch(url).then(r => r.json()); if (list) { await this._loadMods(list); } return; } this.log.i(`Loading mod ${name} from ${url}`); try { /** @type {{default: JadefinMod}} */ const mod = await import(`${url}`); const module = mod.default; if (this._loadingMods_dedupe.delete(module)) { this._loadingMods_dedupe.add(module); throw new Error("Duplicate module"); } this._loadingMods_dedupe.add(module); this.mods[name] = module; this._loadingMods_all.push({module, name, url}); } catch (e) { this.log.e(`Failed to load ${name}`); this.log.dir(e); } })); } async initMods(list) { await Promise.all(this._loadingMods_all.map(async (mod) => { this.log.i(`Initializing mod ${mod.name}`); try { await mod.module.init(mod.name, mod.url); } catch (e) { this.log.e(`Failed to initialize ${mod.name}`); this.log.dir(e); } })); } })());