ARRFLIX/bin/inject-shim.py
s8n d41aaa04fd shim: nukeSettings() drops drawer Settings link for non-admins
CSS selectors in CustomCss (a[href*=mypreferencesmenu], :has(...) wrappers)
weren't reliably hiding the entry — bundle renders it via MUI ListItemButton
+ React Router NavLink and the rendered DOM didn't match the wrapper rules.

Add nukeSettings() to the runtime shim: queries any
a[href*=mypreferencesmenu] / [to*=mypreferencesmenu], walks up to closest
li/.MuiListItem-root/[role=menuitem] and sets display:none. Wired into
start(), a new MutationObserver on document.body, and the existing 1s
setInterval. CustomCss rules left in place as belt-and-braces.

Doc: extend 10-spa-runtime-shim.md with the diagnosis, the bind-mount inode
gotcha (single-file binds + os.replace orphans the container's view), and
the nsenter-based recovery path.
2026-05-08 03:51:48 +01:00

120 lines
4.5 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 nukeSettings(){
try {
var nodes = document.querySelectorAll('a[href*="mypreferencesmenu"], [to*="mypreferencesmenu"]');
for (var i=0;i<nodes.length;i++){
var el = nodes[i];
var p = el.closest && (el.closest('li, .MuiListItem-root, [role="menuitem"]')) || el;
if (p && p.style && p.style.display !== 'none') p.style.display = 'none';
}
} catch(e){}
}
function start(){
lockTitle(); lockFavicon(); nukeSettings();
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){}
try {
if (document.body && window.MutationObserver) {
new MutationObserver(function(){ nukeSettings(); }).observe(document.body, { childList:true, subtree:true });
}
} 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();
nukeSettings();
}, 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)
# Use a callable replacement to bypass backreference parsing in WRAPPED
new_html, n = pattern.subn(lambda _m: 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()