319 lines
9.6 KiB
JavaScript
319 lines
9.6 KiB
JavaScript
|
//@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 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) {
|
||
|
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) {
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// 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(() => this.webpackModuleFuncs);
|
||
|
|
||
|
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.webpackLoad?.(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));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @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));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @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);
|
||
|
}
|
||
|
}));
|
||
|
|
||
|
}
|
||
|
|
||
|
})());
|