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.
120 lines
4.5 KiB
Python
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()
|