jadefin/Jadefin.js

385 lines
14 KiB
JavaScript
Raw Normal View History

2024-03-01 21:01:22 +01:00
//@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<JadefinMod>} */
_loadingMods_dedupe = new Set();
/** @type {{module: JadefinMod, name: String, url: String}[]} */
_loadingMods_all = [];
/** @type {Promise<void>} */
_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) {
2024-05-12 23:21:17 +02:00
const self = this;
2024-03-01 21:01:22 +01:00
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;
}
2024-05-12 23:21:17 +02:00
/** @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.webpackChunkIdToJS = Object.values(this.webpackLoad).find(v => typeof(v) == "function" && v.toString().indexOf(`+".chunk.js"`) != -1);
2024-05-12 23:21:17 +02:00
/** @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");
2024-05-12 23:21:17 +02:00
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}`);
2024-05-12 23:21:17 +02:00
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<any>} */
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 via JSON.stringify(webpackChunk.flatMap(c => c[0])) on the home screen.
const webpackChunkIdsWanted = 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,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,14510,96307,5617,96084,40367,73233,52011,40367,96084,56422,27962,96084,32762,35463,8372,28349,58782,29808,18119,24468,50777,84158,49275,35308,38965]");
// 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)));
2024-05-12 23:21:17 +02:00
// Wait until everything else is ready.
2024-03-01 21:01:22 +01:00
await JadefinUtils.waitUntil(() => this.webpackModuleFuncs);
2024-05-12 23:21:17 +02:00
// this._webpackUnsafeModuleIDs = this.findUnsafeWebpackModules();
2024-03-01 21:01:22 +01:00
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) {
2024-05-12 23:21:17 +02:00
return Object.keys(this.webpackModuleFuncs).map(id => this.webpackTryLoad?.(id)).filter(e => e && cb(e));
2024-03-01 21:01:22 +01:00
}
/**
* @param {(e: any) => any} cb
*/
findWebpackModules(cb) {
2024-05-12 23:21:17 +02:00
return Object.keys(this.webpackModuleFuncs).map(id => this.webpackTryLoad?.(id)?.default).filter(e => e && cb(e));
2024-03-01 21:01:22 +01:00
}
/**
* @param {(e: any) => any} cb
*/
findWebpackFunctions(cb) {
2024-05-12 23:21:17 +02:00
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;
2024-03-01 21:01:22 +01:00
}
/**
* @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);
}
}));
}
})());