ARRFLIX/bin/inject-shim.py
s8n 514dcb6ffc Branding shim: lock <title> + favicon at runtime against SPA overwrites
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.
2026-05-08 03:25:16 +01:00

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()