diff --git a/.gitignore b/.gitignore index 4ea1c0b..6b546e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .htaccess /priv/ /.vscode/ +update.sh diff --git a/Jadefin.js b/Jadefin.js index 8d310fd..27e2d49 100644 --- a/Jadefin.js +++ b/Jadefin.js @@ -41,33 +41,6 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi } initEarly() { - // Required for mods to be able to replace some things. - // Might or might not horribly wreck everything. - const defineProperty = this._Object_defineProperty = Object.defineProperty.bind(Object); - const defineProperties = this._Object_defineProperties = Object.defineProperties.bind(Object); - - Object.defineProperty = (obj, prop, descriptor) => { - if (descriptor && prop != "prototype") { - descriptor.configurable = true; - } - - return defineProperty(obj, prop, descriptor); - }; - - Object.defineProperties = (obj, props) => { - if (props) { - for (const prop of Object.keys(props)) { - const descriptor = props[prop]; - - if (descriptor && prop != "prototype") { - descriptor.configurable = true; - } - } - } - - return defineProperties(obj, props); - }; - // Required for mods to be able to perform certain worker shenanigans. window.Worker = (class Worker extends window.Worker { constructor(scriptURL, options) { @@ -92,6 +65,8 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi } async init(base) { + const self = this; + await super.init("Jadefin", this.modUrl); // Wait until webpackChunk and Emby exist. @@ -108,10 +83,71 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi return; } - // Wait until webpackChunk and Emby exist. - // It might be delayed due to the main jellyfin bundle being replaced by an inject script on Android. + /** @type {(id: number | string) => any} */ + this.webpackTryLoad = id => { + 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.webpackIdToChunkJS = Object.values(this.webpackLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`+".chunk.js"`) != -1); + + /** @type {(id: any) => string} */ + this.webpackIdToChunkCSS = Object.values(this.webpackLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`+".css"`) != -1); + + const webpackIdToChunkJSKey = Object.keys(this.webpackLoad).find(k => this.webpackLoad?.[k] == this.webpackIdToChunkJS); + if (!this.webpackIdToChunkJS || !webpackIdToChunkJSKey) { + this.log.e("Couldn't obtain webpackIdToChunkJS"); + return; + } + + this.webpackLoad[webpackIdToChunkJSKey] = function(id) { + const rv = self.webpackIdToChunkJS?.(id); + self.log.v(`Webpack converted chunk ID to JS name: ${id} -> ${rv}`); + return rv; + }; + + /** @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); + + // HACKFIX: Load all chunks when Jadefin initializes! + // webpackIdToChunkJS contains all IDs either ahead of === or : + // FIXME: This smells like race condition hell! We might be too late for this kind of hooking. + // FIXME: Ideally, don't. This makes startup take ages. + const webpackChunks = [...this.webpackIdToChunkJS.toString().matchAll(/\d+(?=:|=)/g)].map(v => parseInt(v[0])); + await Promise.all(webpackChunks.map(id => this.webpackLoadChunk?.(id))); + + // Wait until everything else is ready. await JadefinUtils.waitUntil(() => this.webpackModuleFuncs); + // this._webpackUnsafeModuleIDs = this.findUnsafeWebpackModules(); + const initing = [ this.initHookHtmlVideoPlayer(), ]; @@ -174,21 +210,41 @@ export default JadefinIntegrity("Jadefin", import.meta.url, () => window["Jadefi * @param {(e: any) => any} cb */ findWebpackRawLoad(cb) { - return Object.keys(this.webpackModuleFuncs).map(id => this.webpackLoad?.(id)).filter(e => e && cb(e)); + 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.webpackLoad?.(id)?.default).filter(e => e && cb(e)); + 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.webpackLoad?.(id)).filter(e => e && e instanceof Function && cb(e)); + 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; } /** diff --git a/JadefinModules.js b/JadefinModules.js index 418c5ab..a2b2540 100644 --- a/JadefinModules.js +++ b/JadefinModules.js @@ -89,13 +89,13 @@ export default JadefinIntegrity("JadefinModules", import.meta.url, () => window[ // escape-html /** @return {(text: string) => string} */ get escapeHtml() { - return this._.escapeHtml ??= this.Jadefin.findWebpackFunctions(e => e.toString().indexOf(`{switch(r.charCodeAt(a)){case 34`) != -1)[0]; + return this._.escapeHtml ??= this.Jadefin.findWebpackFunctions(e => e.toString().indexOf(`charCodeAt(a)){case 34`) != -1)[0]; } // toast /** @return {(text: string) => void} */ get toast() { - return this._.toast ??= this.Jadefin.findWebpackRawLoad(e => (e.Z?.toString().indexOf(`toast"),t.textContent=`) || -1) != -1)[0]?.Z; + return this._.toast ??= this.Jadefin.findWebpackRawLoad(e => (e.A?.toString().indexOf(`toast"),t.textContent=`) || -1) != -1)[0]?.A; } // plugins/syncPlay/core/index @@ -108,7 +108,7 @@ export default JadefinIntegrity("JadefinModules", import.meta.url, () => window[ }} */ get syncPlay() { - return this._.syncPlay ??= this.Jadefin.findWebpackRawLoad(e => e.Z?.Manager?.isSyncPlayEnabled)[0]?.Z; + return this._.syncPlay ??= this.Jadefin.findWebpackModules(e => e.Manager?.isSyncPlayEnabled)[0]; } // inputManager @@ -131,7 +131,7 @@ export default JadefinIntegrity("JadefinModules", import.meta.url, () => window[ @return {any} */ get playbackManager() { - return this._.playbackManager ??= this.Jadefin.findWebpackRawLoad(e => e.O?.canHandleOffsetOnCurrentSubtitle)[0]?.O; + return this._.playbackManager ??= this.Jadefin.findWebpackRawLoad(e => e.f?.canHandleOffsetOnCurrentSubtitle)[0]?.f; } // plugins/htmlVideoPlayer/plugin.js @@ -156,7 +156,7 @@ export default JadefinIntegrity("JadefinModules", import.meta.url, () => window[ } */ get taskButton() { - return this._.taskButton ??= this.Jadefin.findWebpackRawLoad(e => (e.Z?.toString().indexOf(`sendMessage("ScheduledTasksInfoStart","1000,1000")`) || -1) != -1)[0]?.Z; + return this._.taskButton ??= this.Jadefin.findWebpackRawLoad(e => (e.A?.toString().indexOf(`sendMessage("ScheduledTasksInfoStart","1000,1000")`) || -1) != -1)[0]?.A; } // browser @@ -202,7 +202,7 @@ export default JadefinIntegrity("JadefinModules", import.meta.url, () => window[ }} */ get browser() { - return this._.browser ??= this.Jadefin.findWebpackRawLoad(e => e.Z && "supportsCssAnimation" in e.Z && "version" in e.Z)[0]?.Z; + return this._.browser ??= this.Jadefin.findWebpackRawLoad(e => typeof(e.A) == "object" && "supportsCssAnimation" in e.A && "version" in e.A)[0]?.A; } // datetime @@ -220,7 +220,7 @@ export default JadefinIntegrity("JadefinModules", import.meta.url, () => window[ }} */ get datetime() { - return this._.datetime ??= this.Jadefin.findWebpackRawLoad(e => e.ZP?.getDisplayRunningTime)[0]?.ZP; + return this._.datetime ??= this.Jadefin.findWebpackRawLoad(e => e.Ay?.getDisplayRunningTime)[0]?.Ay; } // events diff --git a/JadefinUtils.js b/JadefinUtils.js index 9312c09..4ca7c4e 100644 --- a/JadefinUtils.js +++ b/JadefinUtils.js @@ -29,11 +29,11 @@ export default JadefinIntegrity("JadefinUtils", import.meta.url, () => window["J } get routePath() { - return JadefinModules.Emby.Page.currentRouteInfo.route.path; + return JadefinModules.Emby.Page.currentRouteInfo.path; } get routePathIsVideo() { - return this.routePath == "playback/video/index.html"; + return this.routePath == "/video"; } get currentPlayer() { diff --git a/init.js b/init.js index 286ca3c..9e59aea 100644 --- a/init.js +++ b/init.js @@ -2,6 +2,37 @@ let jadefinInit = /** @type {HTMLScriptElement} */ (document.currentScript); +function jadefinInitExtremelyEarly() { + // Required for mods to be able to replace some things. + // Might or might not horribly wreck everything. + const defineProperty = this._Object_defineProperty = Object.defineProperty.bind(Object); + const defineProperties = this._Object_defineProperties = Object.defineProperties.bind(Object); + + Object.defineProperty = (obj, prop, descriptor) => { + if (descriptor && prop != "prototype") { + descriptor.configurable = true; + } + + return defineProperty(obj, prop, descriptor); + }; + + Object.defineProperties = (obj, props) => { + if (props) { + for (const prop of Object.keys(props)) { + const descriptor = props[prop]; + + if (descriptor && prop != "prototype") { + descriptor.configurable = true; + } + } + } + + return defineProperties(obj, props); + }; +} + +jadefinInitExtremelyEarly(); + (() => { let loadedJellyfin = false; let initedJadefin = false; diff --git a/mods/InputEater.js b/mods/InputEater.js index 7d86c2d..40435a9 100644 --- a/mods/InputEater.js +++ b/mods/InputEater.js @@ -212,7 +212,7 @@ export default JadefinIntegrity("InputEater", import.meta.url, () => new (class } initHookMediaSessionHandlers() { - const owner = Jadefin.findWebpackRawLoad(e => e?.O?.nextTrack && e?.O?.fastForward)[0].O; + const owner = Jadefin.findWebpackRawLoad(e => e?.f?.nextTrack && e?.f?.fastForward)[0].f; if (!owner || !("mediaSession" in navigator)) { this.log.e("Couldn't hook media session handlers"); diff --git a/mods/Transcript.css b/mods/Transcript.css index 225f5ce..1bf3112 100644 --- a/mods/Transcript.css +++ b/mods/Transcript.css @@ -35,7 +35,7 @@ div#videoOsdPage .osdTranscript .line { scroll-snap-align: end; --bg: #000000; --bg-opacity: 0.25; - --bar: rgb(var(--accent, 0x00, 0xad, 0xee)); + --bar: var(--transcript-accent, #00adee); --bar-opacity: 0; } body:not([input-eaten=true]) div#videoOsdPage .osdTranscript .line { diff --git a/mods/jade/PatchForceHLSJS.js b/mods/jade/PatchForceHLSJS.js index f5b3bf1..2756f13 100644 --- a/mods/jade/PatchForceHLSJS.js +++ b/mods/jade/PatchForceHLSJS.js @@ -25,10 +25,10 @@ export default JadefinIntegrity("PatchAndroidHLSJS", import.meta.url, () => new async init(name, url) { await super.init(name, url); - const htmlMediaHelper = this.htmlMediaPlayer = Jadefin.findWebpackRawLoad(e => (e.rR?.toString().indexOf("x-mpegURL") || -1) != -1)[0]; - this._enableHlsJsPlayer = htmlMediaHelper.rR.bind(htmlMediaHelper); + const htmlMediaHelper = this.htmlMediaPlayer = Jadefin.findWebpackRawLoad(e => (e.JQ?.toString().indexOf("x-mpegURL") || -1) != -1)[0]; + this._enableHlsJsPlayer = htmlMediaHelper.JQ.bind(htmlMediaHelper); - Object.defineProperty(htmlMediaHelper, "rR", { get: () => this.enableHlsJsPlayer }); + Object.defineProperty(htmlMediaHelper, "JQ", { get: () => this.enableHlsJsPlayer }); const ExtrasMenu = /** @type {import("../ExtrasMenu.js").default} */ (Jadefin.getMod("ExtrasMenu"));