The static <title>ARRFLIX</title> patch wasn't enough - Jellyfin's bundle calls document.title=... on hydrate and per-route, so the tab kept showing 'Jellyfin'. Add a self-contained inline IIFE in <head> that: - Replaces 'Jellyfin' with 'ARRFLIX' on the title (incl. ' - Jellyfin' suffix) - Pins favicon hrefs to the existing data: URL already in the page - Watches <head> via MutationObserver for SPA churn - Has a 1s setInterval safety net for late-binding navigations - One-shot unregisters the Jellyfin service worker so old clients reload fresh bin/inject-shim.py is the source of truth - idempotent (replaces marker block). docs/10-spa-runtime-shim.md covers root cause, deploy flow, SW eviction, and how to extend the shim on Jellyfin upgrade.
103 lines
3.8 KiB
Python
103 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Inject the ARRFLIX runtime branding shim into web-overrides/index.html.
|
|
Idempotent: if the shim is already present, exits 0 with a notice.
|
|
"""
|
|
import sys, pathlib, re
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
TARGET = ROOT / "web-overrides" / "index.html"
|
|
MARKER_BEGIN = "/* ARRFLIX-SHIM-BEGIN */"
|
|
MARKER_END = "/* ARRFLIX-SHIM-END */"
|
|
|
|
SHIM = MARKER_BEGIN + r"""
|
|
(function(){
|
|
var TITLE = 'ARRFLIX';
|
|
var BARE_RE = /^Jellyfin$/i;
|
|
function getFavicon(){
|
|
var l = document.querySelector('link[rel="shortcut icon"], link[rel="icon"]');
|
|
return l && l.href ? l.href : null;
|
|
}
|
|
function lockTitle(){
|
|
try {
|
|
var t = document.title || '';
|
|
if (BARE_RE.test(t)) { document.title = TITLE; return; }
|
|
if (/Jellyfin/i.test(t)) {
|
|
var cleaned = t.replace(/\s*[-|]\s*Jellyfin\s*$/i, '').replace(/Jellyfin/gi, TITLE);
|
|
if (!cleaned) { document.title = TITLE; }
|
|
else if (!/ARRFLIX/i.test(cleaned)) { document.title = cleaned + ' - ' + TITLE; }
|
|
else { document.title = cleaned; }
|
|
}
|
|
} catch(e){}
|
|
}
|
|
function lockFavicon(){
|
|
try {
|
|
var fav = getFavicon();
|
|
if (!fav || fav.indexOf('data:image') !== 0) return;
|
|
var icons = document.querySelectorAll('link[rel*="icon"]');
|
|
for (var i=0;i<icons.length;i++){ if (icons[i].href !== fav) icons[i].href = fav; }
|
|
} catch(e){}
|
|
}
|
|
function start(){
|
|
lockTitle(); lockFavicon();
|
|
try {
|
|
var head = document.head || document.querySelector('head');
|
|
if (head && window.MutationObserver) {
|
|
new MutationObserver(function(){ lockTitle(); lockFavicon(); }).observe(head, { childList:true, subtree:true, characterData:true, attributes:true, attributeFilter:['href'] });
|
|
}
|
|
} catch(e){}
|
|
setInterval(function(){
|
|
var t = document.title || '';
|
|
if (BARE_RE.test(t) || /Jellyfin/i.test(t)) lockTitle();
|
|
var fav = getFavicon();
|
|
if (fav && fav.indexOf('data:image') !== 0) lockFavicon();
|
|
}, 1000);
|
|
}
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', start, { once:true });
|
|
} else { start(); }
|
|
if ('serviceWorker' in navigator) {
|
|
try {
|
|
navigator.serviceWorker.getRegistrations().then(function(regs){
|
|
regs.forEach(function(r){
|
|
try {
|
|
var url = (r.active && r.active.scriptURL) || '';
|
|
if (url.indexOf('serviceworker.js') !== -1) { r.unregister(); }
|
|
} catch(e){}
|
|
});
|
|
}).catch(function(){});
|
|
if (window.caches && caches.keys) {
|
|
caches.keys().then(function(keys){ keys.forEach(function(k){ caches.delete(k); }); }).catch(function(){});
|
|
}
|
|
} catch(e){}
|
|
}
|
|
})();
|
|
""" + MARKER_END
|
|
|
|
WRAPPED = "<script>" + SHIM + "</script>"
|
|
|
|
def main():
|
|
html = TARGET.read_text(encoding="utf-8")
|
|
if MARKER_BEGIN in html:
|
|
# Replace the existing shim block in place
|
|
pattern = re.compile(r"<script>" + re.escape(MARKER_BEGIN) + r".*?" + re.escape(MARKER_END) + r"</script>", re.DOTALL)
|
|
new_html, n = pattern.subn(WRAPPED, html, count=1)
|
|
if n != 1:
|
|
print("ERROR: shim markers present but could not match for replacement", file=sys.stderr)
|
|
sys.exit(2)
|
|
TARGET.write_text(new_html, encoding="utf-8")
|
|
print(f"Replaced existing shim ({len(WRAPPED)} chars).")
|
|
return
|
|
# Insert immediately after <head>
|
|
needle = "<head>"
|
|
idx = html.find(needle)
|
|
if idx < 0:
|
|
print("ERROR: <head> not found in target", file=sys.stderr)
|
|
sys.exit(1)
|
|
insert_at = idx + len(needle)
|
|
new_html = html[:insert_at] + WRAPPED + html[insert_at:]
|
|
TARGET.write_text(new_html, encoding="utf-8")
|
|
print(f"Inserted shim ({len(WRAPPED)} chars) after <head> at offset {insert_at}.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|