diff --git a/injector.js b/injector.js index 4c146cb..f06e985 100644 --- a/injector.js +++ b/injector.js @@ -1190,7 +1190,9 @@ console.log('[KefinTweaks Injector] Jellyfin major version:', majorVersion ?? 'unknown'); const config = window.KefinTweaksConfig || {}; - const root = Loader.normalizeRoot(config.kefinTweaksRoot || ''); + const root = Loader.getResolvedKefinRoot + ? Loader.getResolvedKefinRoot(config) + : Loader.normalizeRoot(config.kefinTweaksRoot || ''); try { const plan = Loader.buildLoadPlan(config, majorVersion, { diff --git a/kefinTweaks-plugin.js b/kefinTweaks-plugin.js index 6040523..ec4f64d 100644 --- a/kefinTweaks-plugin.js +++ b/kefinTweaks-plugin.js @@ -205,7 +205,8 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; let injectorScriptContent = null; try { const rootRaw = config.kefinTweaksRoot || ''; - const root = rootRaw.endsWith('/') ? rootRaw : rootRaw + '/'; + const resolvedRaw = config.kefinTweaksRootResolved || rootRaw; + const root = resolvedRaw.endsWith('/') ? resolvedRaw : resolvedRaw + '/'; if (root && root !== '/') { await loadKefinTweaksLoaderFromRoot(root); const Loader = window.KefinTweaksLoader; @@ -217,8 +218,11 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; } catch (e) { console.warn('[KefinTweaks Installer] Could not resolve Jellyfin major for injector bake:', e); } + const planRoot = Loader.getResolvedKefinRoot + ? Loader.getResolvedKefinRoot(config) + : root; const plan = Loader.buildLoadPlan(config, major, { - root, + root: planRoot, configOnly: config.enabled === false }); injectorScriptContent = Loader.buildInjectorScript(plan); @@ -334,8 +338,20 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; } try { - const root = rootRaw.endsWith('/') ? rootRaw : rootRaw + '/'; - await loadKefinTweaksLoaderFromRoot(root); + const normalize = (r) => { + if (!r || typeof r !== 'string') return ''; + return r.endsWith('/') ? r : r + '/'; + }; + + const expected = normalize(await resolveRootVersion(rootRaw)); + const current = normalize(config.kefinTweaksRootResolved || ''); + if (!current || current !== expected) { + config.kefinTweaksRootResolved = expected; + console.log('[KefinTweaks Installer] Updated kefinTweaksRootResolved:', current || '(none)', '->', expected); + } + + const loadRoot = normalize(config.kefinTweaksRootResolved || rootRaw); + await loadKefinTweaksLoaderFromRoot(loadRoot); const Loader = window.KefinTweaksLoader; if (!Loader) { return { ok: false, reason: 'KefinTweaksLoader unavailable' }; @@ -349,8 +365,9 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; console.warn('[KefinTweaks Installer] Could not resolve Jellyfin major for auto-create:', e); } + const resolvedRoot = Loader.getResolvedKefinRoot(config); const plan = Loader.buildLoadPlan(config, major, { - root, + root: resolvedRoot, configOnly: config.enabled === false }); const injectorScriptContent = Loader.buildInjectorScript(plan); @@ -898,6 +915,10 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; } const kefinTweaksRoot = buildKefinTweaksRootUrl(sourceType, source); + const kefinTweaksRootResolvedRaw = await resolveRootVersion(kefinTweaksRoot); + const kefinTweaksRootResolved = kefinTweaksRootResolvedRaw.endsWith('/') + ? kefinTweaksRootResolvedRaw + : kefinTweaksRootResolvedRaw + '/'; // Load default config from the selected root let defaultConfig; @@ -908,6 +929,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; // Create minimal config if loading fails defaultConfig = { kefinTweaksRoot: kefinTweaksRoot, + kefinTweaksRootResolved: kefinTweaksRootResolved, enabled: true, scripts: {}, exclusiveElsewhere: {}, @@ -929,6 +951,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; kefinTweaksRoot: kefinTweaksRoot, ...(currentConfig || {}), kefinTweaksRoot: kefinTweaksRoot, // Ensure root is updated + kefinTweaksRootResolved: kefinTweaksRootResolved, enabled: enabledState // Update enabled state }; diff --git a/scripts/kefinTweaks-loader.js b/scripts/kefinTweaks-loader.js index 16af3be..d87a4c5 100644 --- a/scripts/kefinTweaks-loader.js +++ b/scripts/kefinTweaks-loader.js @@ -641,6 +641,14 @@ return root.endsWith('/') ? root : root + '/'; } + /** + * Prefer kefinTweaksRootResolved (commit SHA / verified URL); fall back to symbolic kefinTweaksRoot. + */ + function getResolvedKefinRoot(config) { + const cfg = config || {}; + return normalizeRoot(cfg.kefinTweaksRootResolved || cfg.kefinTweaksRoot || ''); + } + function resolveAssetUrl(root, relativePath) { if (!relativePath) return null; if (/^https?:\/\//i.test(relativePath)) return relativePath; @@ -677,26 +685,29 @@ function expandScriptToAssets(scriptDef, root, urlSuffix, thirdParty) { const assets = []; const suffix = urlSuffix || ''; + const pushRel = (kind, relativePath, name) => { + if (!relativePath) return; + if (/^https?:\/\//i.test(relativePath)) { + assets.push({ kind: kind, url: relativePath, name: name }); + return; + } + const url = resolveAssetUrl(root, relativePath); + if (!url) return; + assets.push({ + kind: kind, + url: url + suffix, + path: relativePath, + name: name + }); + }; for (const tpName of scriptDef.thirdParty || []) { const tp = thirdParty[tpName]; if (!tp) continue; - (tp.css || []).forEach((css) => { - const url = resolveAssetUrl(root, css); - if (url) assets.push({ kind: 'css', url: url + (/^https?:/i.test(css) ? '' : suffix), name: tpName }); - }); - (tp.js || []).forEach((js) => { - const url = resolveAssetUrl(root, js); - if (url) assets.push({ kind: 'js', url: url + (/^https?:/i.test(js) ? '' : suffix), name: tpName }); - }); - } - if (scriptDef.css) { - const url = resolveAssetUrl(root, scriptDef.css); - if (url) assets.push({ kind: 'css', url: url + suffix, name: scriptDef.name }); - } - if (scriptDef.script) { - const url = resolveAssetUrl(root, scriptDef.script); - if (url) assets.push({ kind: 'js', url: url + suffix, name: scriptDef.name }); + (tp.css || []).forEach((css) => pushRel('css', css, tpName)); + (tp.js || []).forEach((js) => pushRel('js', js, tpName)); } + if (scriptDef.css) pushRel('css', scriptDef.css, scriptDef.name); + if (scriptDef.script) pushRel('js', scriptDef.script, scriptDef.name); return assets; } @@ -742,7 +753,11 @@ function buildLoadPlan(config, majorVersion, options) { options = options || {}; - const root = normalizeRoot(options.root || (config && config.kefinTweaksRoot) || ''); + const root = normalizeRoot( + options.root + || getResolvedKefinRoot(config) + || '' + ); if (!root) throw new Error('kefinTweaksRoot is required to build load plan'); const urlSuffix = options.urlSuffix || ''; const byName = definitionsByName(); @@ -764,7 +779,7 @@ const seen = new Set(); const pushAssets = (list) => { list.forEach((a) => { - const key = a.kind + '|' + a.url; + const key = a.kind + '|' + (a.url || a.path || ''); if (seen.has(key)) return; seen.add(key); assets.push(a); @@ -778,8 +793,18 @@ }); pushAssets([ - { kind: 'css', url: resolveAssetUrl(root, CONFIGURATION_FILES.css) + urlSuffix, name: 'configuration' }, - { kind: 'js', url: resolveAssetUrl(root, CONFIGURATION_FILES.script) + urlSuffix, name: 'configuration' } + { + kind: 'css', + url: resolveAssetUrl(root, CONFIGURATION_FILES.css) + urlSuffix, + path: CONFIGURATION_FILES.css, + name: 'configuration' + }, + { + kind: 'js', + url: resolveAssetUrl(root, CONFIGURATION_FILES.script) + urlSuffix, + path: CONFIGURATION_FILES.script, + name: 'configuration' + } ]); const stamp = [ @@ -907,7 +932,22 @@ } function buildInjectorScript(loadPlan) { - const assetsJson = JSON.stringify(loadPlan.assets || []); + // Bake relative paths (and absolute http urls); join with resolved root at runtime + const bakeAssets = (loadPlan.assets || []).map((a) => { + if (!a) return null; + if (a.url && /^https?:\/\//i.test(a.url) && !a.path) { + return { kind: a.kind, url: a.url, name: a.name }; + } + if (a.path) { + return { kind: a.kind, path: a.path, name: a.name }; + } + if (a.url) { + return { kind: a.kind, url: a.url, name: a.name }; + } + return null; + }).filter(Boolean); + + const assetsJson = JSON.stringify(bakeAssets); const defsJson = JSON.stringify(loadPlan.definitions || SCRIPT_DEFINITIONS); const stamp = loadPlan.stamp || ''; const stampJson = JSON.stringify(stamp); @@ -916,11 +956,22 @@ '// kefinTweaksInjectorStamp=' + stamp, '(function () {', ' if (window.KefinTweaksScriptsPreloaded) return;', - ' var root = (window.KefinTweaksConfig && window.KefinTweaksConfig.kefinTweaksRoot) || \'\';', - ' var isExperimental = /@experimental(\\/|$)/i.test(String(root));', - ' var isOfficialCdn = /cdn\\.jsdelivr\\.net\\/gh\\/ranaldsgift\\/KefinTweaks@/i.test(String(root));', - ' if (!String(root).trim()) return;', + ' var cfg = window.KefinTweaksConfig || {};', + ' if (cfg.enabled !== true) return;', + ' var symbolicRoot = cfg.kefinTweaksRoot || \'\';', + ' var isExperimental = /@experimental(\\/|$)/i.test(String(symbolicRoot));', + ' var isOfficialCdn = /cdn\\.jsdelivr\\.net\\/gh\\/ranaldsgift\\/KefinTweaks@/i.test(String(symbolicRoot));', + ' if (!String(symbolicRoot).trim()) return;', ' if (!isExperimental && isOfficialCdn) return;', + ' var resolvedRoot = String(cfg.kefinTweaksRootResolved || cfg.kefinTweaksRoot || \'\').trim();', + ' if (!resolvedRoot) return;', + ' if (resolvedRoot.charAt(resolvedRoot.length - 1) !== \'/\') resolvedRoot += \'/\';', + ' function joinAssetUrl(relativePath) {', + ' if (!relativePath) return null;', + ' if (/^https?:\\/\\//i.test(relativePath)) return relativePath;', + ' var base = resolvedRoot + \'scripts/\';', + ' try { return new URL(relativePath, base).href; } catch (e) { return base + relativePath; }', + ' }', ' window.KefinTweaksScriptsPreloaded = true;', ' window.KefinTweaksInjectorStamp = ' + stampJson + ';', ' var SCRIPT_DEFINITIONS_BAKED = ' + defsJson + ';', @@ -972,18 +1023,20 @@ ' var assets = ' + assetsJson + ';', ' for (var i = 0; i < assets.length; i++) {', ' var a = assets[i];', - ' if (!a || !a.url) continue;', + ' if (!a) continue;', + ' var href = a.url || joinAssetUrl(a.path);', + ' if (!href) continue;', ' if (a.kind === \'css\') {', - ' if (document.querySelector(\'link[href="\' + a.url + \'"]\')) continue;', + ' if (document.querySelector(\'link[href="\' + href + \'"]\')) continue;', ' var l = document.createElement(\'link\');', ' l.rel = \'stylesheet\';', ' l.type = \'text/css\';', - ' l.href = a.url;', + ' l.href = href;', ' document.head.appendChild(l);', ' } else {', - ' if (document.querySelector(\'script[src="\' + a.url + \'"]\')) continue;', + ' if (document.querySelector(\'script[src="\' + href + \'"]\')) continue;', ' var s = document.createElement(\'script\');', - ' s.src = a.url;', + ' s.src = href;', ' s.async = false;', ' document.head.appendChild(s);', ' }', @@ -1183,6 +1236,7 @@ isScriptCompatible: isScriptCompatible, mergeEnabledScripts: mergeEnabledScripts, normalizeRoot: normalizeRoot, + getResolvedKefinRoot: getResolvedKefinRoot, resolveAssetUrl: resolveAssetUrl, buildLoadPlan: buildLoadPlan, buildInjectorScript: buildInjectorScript, diff --git a/scripts/utils.js b/scripts/utils.js index 5eef807..b4a4c14 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -2213,9 +2213,10 @@ window.KefinTweaksConfig = ${JSON.stringify(configToSave, null, 2)};`; // Build / refresh KefinTweaks-injector preload entry (same POST) let injectorScriptContent = null; try { - const root = (configToSave.kefinTweaksRoot || '').endsWith('/') - ? configToSave.kefinTweaksRoot - : (configToSave.kefinTweaksRoot || '') + '/'; + const resolvedOrSymbolic = configToSave.kefinTweaksRootResolved || configToSave.kefinTweaksRoot || ''; + const root = resolvedOrSymbolic.endsWith('/') + ? resolvedOrSymbolic + : resolvedOrSymbolic + '/'; if (root && root !== '/') { if (!window.KefinTweaksLoader) { await new Promise((resolve, reject) => { @@ -2237,8 +2238,11 @@ window.KefinTweaksConfig = ${JSON.stringify(configToSave, null, 2)};`; if (Loader) { Loader.ensureKefinTweaksApi(Loader.SCRIPT_DEFINITIONS); const major = await window.KefinTweaks.getJellyfinMajorVersion(); + const planRoot = Loader.getResolvedKefinRoot + ? Loader.getResolvedKefinRoot(configToSave) + : root; const plan = Loader.buildLoadPlan(configToSave, major, { - root, + root: planRoot, configOnly: configToSave.enabled === false }); injectorScriptContent = Loader.buildInjectorScript(plan);