diff --git a/Jadefin.js b/Jadefin.js index 706d582..1a94b7c 100644 --- a/Jadefin.js +++ b/Jadefin.js @@ -24,6 +24,33 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi /** @type {any} */ _webpackModuleFuncs; + _webpackBlacklistClient = JSON.parse(localStorage.getItem("jadefin-blacklist") || "[]"); + /** @type {typeof this._webpackBlacklistClient} */ + webpackBlacklistClient = new Proxy(this._webpackBlacklistClient, { + deleteProperty: (target, property) => { + delete target[property]; + + localStorage.setItem("jadefin-blacklist", JSON.stringify(this._webpackBlacklistClient)); + + return true; + }, + + set: (target, property, value, receiver) => { + target[property] = value; + + localStorage.setItem("jadefin-blacklist", JSON.stringify(this._webpackBlacklistClient)); + + return true; + } + }); + + // To update, Jadefin.webpackBlacklistClient.push(0); and update this after a few reloads: JSON.stringify(Jadefin.webpackBlacklistClient) + // Once done: Jadefin.webpackBlacklistClient.splice(0, Jadefin.webpackBlacklistClient.length) + webpackBlacklistDefault = JSON.parse(`[0,"activity","node_modules.@mui.x-date-pickers","15088","node_modules.@mui.utils","node_modules.@mui.material","node_modules.react-transition-group","node_modules.date-fns.esm","23416","livetvtuner","syncPlay-core-players-GenericPlayer","47472","node_modules.@mui.system","55802"]`); + + /** @type {{[id: string]: any}} */ + _webpackCache = {}; + constructor() { super(); @@ -42,6 +69,12 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi } initEarly() { + if (this.webpackBlacklistClient.length) { + this.webpackBlacklist = this.webpackBlacklistClient; + } else { + this.webpackBlacklist = this.webpackBlacklistDefault; + } + // Required for mods to be able to perform certain worker shenanigans. window.Worker = (class Worker extends window.Worker { constructor(scriptURL, options) { @@ -63,6 +96,41 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi })); } }); + + // Required with newer versions of Jellyfin and Jadefin: Fetch and patch runtime.bundle.js ourselves. + // Thankfully this can happen async, Jellyfin will load whenever this is done. + const runtimeBundleScript = Array.from(document.getElementsByTagName("script")).find(s => s.src.indexOf("runtime.jadefin.bundle.js") != -1); + if (!runtimeBundleScript) { + this._runtimeBundlePatched = false; + this.log.e("Using vanilla runtime.bundle.js - some mods might not work properly!"); + } else { + this._runtimeBundlePatched = true; + const url = runtimeBundleScript.src.replace("runtime.jadefin.bundle.js", "runtime.bundle.js"); + this.log.i(`Fetching runtime.bundle.js from ${url}`); + fetch(url).then(r => r.text()).then(async src => { + this.log.i("Patching runtime.bundle.js"); + this._runtimeBundleOrig = src; + + try { + // __webpack_require__ in Compilation.js as reference + // var d=f[e]={id:e,loaded:!1,exports:{}}; in minified version + const matchRequire = src.match(/var [^=]+=([^\[]+)\[[^\[]+\]=\{[^}]*(?:id:[^;]+|loaded:[^;]+|exports:{}[^;]+){3};/); + if (!matchRequire || matchRequire.index === undefined) { + throw new Error("Couldn't find __webpack_require__"); + } + + src = src.substring(0, matchRequire.index) + `window["_JadefinWebpackCache"]=${matchRequire[1]};` + src.substring(matchRequire.index); + + this.log.i("Running patched runtime.bundle.js"); + eval(src); + } catch (e) { + this._runtimeBundlePatched = false; + this.log.e("Patched runtime.bundle.js errored, running vanilla runtime.bundle.js as fallback"); + this.log.dir(e); + eval(this._runtimeBundleOrig); + } + }); + } } async init(base) { @@ -72,79 +140,100 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi // 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"]); + await JadefinUtils.waitUntil(() => window["webpackChunk"] && window["Emby"] && (!this._runtimeBundlePatched || window["_JadefinWebpackCache"])); + + this._webpackRawCache = window["_JadefinWebpackCache"]; + if (!this._webpackRawCache) { + this.log.w("Couldn't obtain _webpackRawCache"); + } window["webpackChunk"].push([["custom_init"], {}, a => { /** @type {((id: number | string) => any & {[key: string]: any})} */ - this.webpackLoad = a; + this.webpackRawLoad = a; }]); - if (!this.webpackLoad) { - this.log.e("Couldn't obtain webpackLoad"); + if (!this.webpackRawLoad) { + this.log.e("Couldn't obtain webpackRawLoad"); return; } /** @type {(id: number | string) => any} */ - this.webpackTryLoad = id => { + this.webpackLoad = id => { const sid = `${id}`; - if (!document.querySelector(".mainDrawer")) { - 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("syncPlay-ui-") || - c.startsWith("35463.") || - c.startsWith("1998.") || - c.startsWith("73233.") - ))) { + const cached = this._webpackCache[sid]; + if (cached) { + return cached; + } + + const name = this.webpackIdToName(id); + + if (this.webpackBlacklist !== this.webpackBlacklistClient) { + const cachedRaw = this._webpackRawCache?.[sid]; + if (cachedRaw && cachedRaw.loaded) { + return this._webpackCache[sid] = cachedRaw.exports; + } + + // Some modules are known to break things when loaded too early. + if (name.startsWith("session-login") || + name.startsWith("syncPlay-ui-") || + name.startsWith("user-display") || + name.startsWith("activity") || + name.startsWith("node_modules.@juggle") || + name.startsWith("node_modules.@mui") || + false + ) { return null; } } + // Some modules shall only be loaded via their intended codepaths. + if (this.webpackBlacklist.indexOf(name) != -1) { + return null; + } + try { - return this.webpackLoad?.(id); + return this._webpackCache[sid] = this.webpackRawLoad?.(id); } catch (e) { - this.log.e(`Failed to load webpack module ${id}`); + this.log.e(`Failed to load webpack module ${id} (${name})`); this.log.dir(e); + this.webpackBlacklist.push(name); return null; } }; /** @type {(id: any) => string} */ - this.webpackChunkIdToJS = Object.values(this.webpackLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`+".chunk.js"`) != -1); + this.webpackChunkIdToJS = Object.values(this.webpackRawLoad).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); + this.webpackIdToCSS = Object.values(this.webpackRawLoad).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"); + this.webpackPreloadChunkLowLevel = Object.values(this.webpackRawLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`document.head.appendChild`) != -1); + const webpackPreloadChunkLowLevelKey = Object.keys(this.webpackRawLoad).find(k => this.webpackRawLoad?.[k] == this.webpackPreloadChunkLowLevel); + if (!this.webpackPreloadChunkLowLevel || !webpackPreloadChunkLowLevelKey) { + this.log.e("Couldn't obtain webpackPreloadChunkLowLevel"); return; } - this.webpackLoad[webpackLoadChunkLowLevelKey] = function(name, cb, sid, id) { + this.webpackRawLoad[webpackPreloadChunkLowLevelKey] = function(name, cb, sid, id) { const _cb = cb; - // self.log.v(`Webpack loading chunk ${id} (${sid}) from ${name}`); + // self.log.v(`Webpack preloading chunk ${id} (${sid}) from ${name}`); cb = (event) => { - self.log.v(`Webpack loaded chunk ${id} (${sid}) from ${name}: ${event.type}`); + self.log.v(`Webpack preloaded chunk ${id} (${sid}) from ${name}: ${event.type}`); return _cb(event); }; - const rv = self.webpackLoadChunkLowLevel?.(name, cb, sid, id); + const rv = self.webpackPreloadChunkLowLevel?.(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); + this.webpackPreloadChunk = Object.values(this.webpackRawLoad).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 = {}; @@ -152,29 +241,6 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi 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("[45642,1451,23247,60815,62155,16304,1270,81771,9886,55931,12036,59928,33067,44965,22940,56401,7495,86897,17060,67224,67622,94048,36933,82363,36546,85500,7011,80183,60232,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,79754,89409,69285,43091,64380,90186,40810,87903,70118,64633,10905,68672,19842,71318,70555,41542,86040,4836,49087,64706,13151,65849,80835,14510,96307,30357,6270,19907,55125,83354,49755,60138,927,10672,2,56577,29593,1680,39573,40394,59258,39232,40465,18395,48979,68413,78938,55802,39435,28567,79617,1998,15605,21857,44184,22424,91737,40367,96084,73233,52011,5617,56422,45568,27962,32762,8372,28349,29808,58782,24468,18119,50777,84158,49275,35308,38965,11136,14044,22943,27018,30010,39548,63365,94115,70594,98574,59015,58122,66935,28312,84430,88788,86355,57949,18084,35463]"), - ].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("activity.") || - js.startsWith("node_modules.@juggle.") || - js.startsWith("node_modules.@mui.") || - 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); @@ -235,28 +301,28 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi } get webpackModuleFuncs() { - return this._webpackModuleFuncs ??= this.webpackLoad ? Object.values(this.webpackLoad).find(v => typeof(v) == "object" && Object.keys(v).length > 10) : null; + return this._webpackModuleFuncs ??= this.webpackRawLoad ? Object.values(this.webpackRawLoad).find(v => typeof(v) == "object" && Object.keys(v).length > 10) : null; } /** * @param {(e: any) => any} cb */ findWebpackRawLoad(cb) { - return JadefinUtils.filterMap(Object.keys(this.webpackModuleFuncs).map(id => this.webpackTryLoad?.(id)), e => e && cb(e)); + return JadefinUtils.filterMap(Object.keys(this.webpackModuleFuncs).map(id => this.webpackLoad?.(id)), e => e && cb(e)); } /** * @param {(e: any) => any} cb */ findWebpackModules(cb) { - return JadefinUtils.filterMap(Object.keys(this.webpackModuleFuncs).map(id => this.webpackTryLoad?.(id)?.default), e => e && cb(e)); + return JadefinUtils.filterMap(Object.keys(this.webpackModuleFuncs).map(id => this.webpackLoad?.(id)?.default), e => e && cb(e)); } /** * @param {(e: any) => any} cb */ findWebpackFunctions(cb) { - return JadefinUtils.filterMap(Object.keys(this.webpackModuleFuncs).map(id => this.webpackTryLoad?.(id)), e => e && e instanceof Function && cb(e)); + return JadefinUtils.filterMap(Object.keys(this.webpackModuleFuncs).map(id => this.webpackLoad?.(id)), e => e && e instanceof Function && cb(e)); } /** @@ -267,10 +333,10 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi for (const id of Object.keys(modules || this.webpackModuleFuncs)) { try { - this.webpackLoad?.(id); + this.webpackRawLoad?.(id); } catch (e) { this.log.w(`Failed to load webpack module ${id}`); - this.log.w(this.webpackModuleFuncs[id].toString()); + this.log.dir(this.webpackModuleFuncs[id]); this.log.dir(e); unsafe.push(id); } @@ -279,6 +345,64 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi return unsafe; } + /** + * @param {any} id + * @return {string[]} + */ + webpackIdToJSs(id) { + if (!this.webpackChunkIdToJS) { + return [ `${id}.unknown.chunk.js` ]; + } + + const sid = `${id}`; + + const meta = window["webpackChunk"].find(c => c[1][sid]); + if (!meta) { + return [ this.webpackChunkIdToJS(parseInt(sid)) ]; + } + + const jss = []; + + for (const pid of meta[0]) { + jss.push(this.webpackChunkIdToJS(pid)); + } + + for (const cid of Object.keys(meta[1])) { + jss.push(this.webpackChunkIdToJS(cid)); + } + + return jss; + }; + + /** + * @param {any} id + * @return {string} + */ + webpackIdToJS(id) { + const jss = this.webpackIdToJSs(id); + + return jss.find(c => !/^\d/.test(c)) || jss.find(c => c.startsWith(`${id}.`)) || `${id}.unknown.chunk.js`; + }; + + /** + * @param {any} id + * @return {string} + */ + webpackIdToName(id) { + const name = this.webpackIdToJS(id); + + if (name.endsWith(".bundle.js")) { + return name.substring(0, name.length - ".bundle.js".length); + } + + if (name.endsWith(".chunk.js")) { + const split = name.lastIndexOf(".", name.length - ".chunk.js".length - 1); + return name.substring(0, split); + } + + return name; + }; + /** * @param {string} name */ diff --git a/JadefinLog.js b/JadefinLog.js index fcce9ea..620c820 100644 --- a/JadefinLog.js +++ b/JadefinLog.js @@ -65,7 +65,11 @@ export default JadefinIntegrity("JadefinLog", import.meta.url, () => class Jadef } if (args.length == 1) { - console.dir(args[0]); + if (args[0] instanceof Error) { + console.error(args[0]); + } else { + console.dir(args[0]); + } return this; } diff --git a/JadefinUtils.js b/JadefinUtils.js index 1de55f6..82f9374 100644 --- a/JadefinUtils.js +++ b/JadefinUtils.js @@ -29,7 +29,7 @@ export default JadefinIntegrity("JadefinUtils", import.meta.url, () => window["J } get routePath() { - return JadefinModules.Emby.Page.currentRouteInfo.path; + return JadefinModules.Emby.Page.currentRouteInfo?.path; } get routePathIsVideo() {