2026-05-08 03:25:16 +01:00
|
|
|
#!/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;
|
2026-05-08 17:04:03 +01:00
|
|
|
/* === English-lockdown (synchronous, runs before Jellyfin bundle) ===
|
|
|
|
|
Pins UI locale to en-US so the SPA never reads navigator.language
|
|
|
|
|
or the user's stored preference. Belt-and-braces against:
|
|
|
|
|
- localStorage keys the SPA reads on boot
|
|
|
|
|
- navigator.language / navigator.languages getters
|
|
|
|
|
- fetch / XHR Accept-Language header (best-effort; most browsers
|
|
|
|
|
block JS from setting it, but Jellyfin sometimes does)
|
|
|
|
|
- user-config save round-trip (rewrite UICulture → en-US before send) */
|
|
|
|
|
try {
|
|
|
|
|
var LS_KEYS = ['appLanguage','selectedlanguage','selectedlocale','language','locale','culture'];
|
|
|
|
|
for (var i=0;i<LS_KEYS.length;i++){
|
|
|
|
|
try { localStorage.setItem(LS_KEYS[i], 'en-US'); } catch(e){}
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
|
|
|
|
try {
|
|
|
|
|
var EN = ['en-US','en'];
|
|
|
|
|
Object.defineProperty(Navigator.prototype, 'language', { get:function(){return 'en-US';}, configurable:true });
|
|
|
|
|
Object.defineProperty(Navigator.prototype, 'languages', { get:function(){return EN.slice();}, configurable:true });
|
|
|
|
|
} catch(e){
|
|
|
|
|
/* fallback for engines that won't let us redefine on the prototype */
|
|
|
|
|
try { Object.defineProperty(navigator, 'language', { get:function(){return 'en-US';}, configurable:true }); } catch(e2){}
|
|
|
|
|
try { Object.defineProperty(navigator, 'languages', { get:function(){return ['en-US','en'];}, configurable:true }); } catch(e2){}
|
|
|
|
|
}
|
|
|
|
|
/* fetch wrapper: strip Accept-Language on outbound requests, and rewrite
|
|
|
|
|
any user-config save body so UICulture is pinned to en-US. */
|
|
|
|
|
try {
|
|
|
|
|
if (window.fetch) {
|
|
|
|
|
var _origFetch = window.fetch;
|
|
|
|
|
window.fetch = function(input, init){
|
|
|
|
|
try {
|
|
|
|
|
init = init || {};
|
|
|
|
|
/* strip Accept-Language if present on a plain object headers init */
|
|
|
|
|
if (init.headers) {
|
|
|
|
|
if (init.headers instanceof Headers) {
|
|
|
|
|
try { init.headers.delete('Accept-Language'); } catch(e){}
|
|
|
|
|
} else if (typeof init.headers === 'object') {
|
|
|
|
|
for (var k in init.headers){ if (k && k.toLowerCase() === 'accept-language') { try { delete init.headers[k]; } catch(e){} } }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/* rewrite user-config save: POST /Users/{id}/Configuration */
|
|
|
|
|
var url = (typeof input === 'string') ? input : (input && input.url) || '';
|
|
|
|
|
var method = (init.method || (input && input.method) || 'GET').toUpperCase();
|
|
|
|
|
if (url && /\/Users\/[^/]+\/Configuration(\?|$)/.test(url) && method === 'POST' && init.body) {
|
|
|
|
|
try {
|
|
|
|
|
var body = init.body;
|
|
|
|
|
if (typeof body === 'string') {
|
|
|
|
|
var obj = JSON.parse(body);
|
|
|
|
|
if (obj && typeof obj === 'object') {
|
|
|
|
|
obj.UICulture = 'en-US';
|
|
|
|
|
init.body = JSON.stringify(obj);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
|
|
|
|
return _origFetch.call(this, input, init);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
|
|
|
|
/* XHR wrapper: strip Accept-Language; rewrite user-config save body. */
|
|
|
|
|
try {
|
|
|
|
|
if (window.XMLHttpRequest) {
|
|
|
|
|
var _open = XMLHttpRequest.prototype.open;
|
|
|
|
|
var _setHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
|
|
|
var _send = XMLHttpRequest.prototype.send;
|
|
|
|
|
XMLHttpRequest.prototype.open = function(method, url){
|
|
|
|
|
this.__arrflix_method = (method || 'GET').toUpperCase();
|
|
|
|
|
this.__arrflix_url = url || '';
|
|
|
|
|
return _open.apply(this, arguments);
|
|
|
|
|
};
|
|
|
|
|
XMLHttpRequest.prototype.setRequestHeader = function(name, value){
|
|
|
|
|
if (name && String(name).toLowerCase() === 'accept-language') return;
|
|
|
|
|
return _setHeader.apply(this, arguments);
|
|
|
|
|
};
|
|
|
|
|
XMLHttpRequest.prototype.send = function(body){
|
|
|
|
|
try {
|
|
|
|
|
if (this.__arrflix_url && /\/Users\/[^/]+\/Configuration(\?|$)/.test(this.__arrflix_url) && this.__arrflix_method === 'POST' && typeof body === 'string') {
|
|
|
|
|
try {
|
|
|
|
|
var obj = JSON.parse(body);
|
|
|
|
|
if (obj && typeof obj === 'object') {
|
|
|
|
|
obj.UICulture = 'en-US';
|
|
|
|
|
body = JSON.stringify(obj);
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
|
|
|
|
return _send.call(this, body);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
|
|
|
|
/* Re-pin localStorage on every visibility change (SPA may rewrite on user save) */
|
|
|
|
|
function pinLocale(){
|
|
|
|
|
try {
|
|
|
|
|
var L = ['appLanguage','selectedlanguage','selectedlocale','language','locale','culture'];
|
|
|
|
|
for (var i=0;i<L.length;i++){ try { if (localStorage.getItem(L[i]) !== 'en-US') localStorage.setItem(L[i], 'en-US'); } catch(e){} }
|
|
|
|
|
} catch(e){}
|
|
|
|
|
}
|
|
|
|
|
/* === end english-lockdown synchronous block === */
|
2026-05-08 03:25:16 +01:00
|
|
|
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){}
|
|
|
|
|
}
|
2026-05-08 03:51:48 +01:00
|
|
|
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){}
|
|
|
|
|
}
|
2026-05-08 03:25:16 +01:00
|
|
|
function start(){
|
2026-05-08 17:04:03 +01:00
|
|
|
lockTitle(); lockFavicon(); nukeSettings(); pinLocale();
|
2026-05-08 03:25:16 +01:00
|
|
|
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){}
|
2026-05-08 03:51:48 +01:00
|
|
|
try {
|
|
|
|
|
if (document.body && window.MutationObserver) {
|
|
|
|
|
new MutationObserver(function(){ nukeSettings(); }).observe(document.body, { childList:true, subtree:true });
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
2026-05-08 03:25:16 +01:00
|
|
|
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();
|
2026-05-08 03:51:48 +01:00
|
|
|
nukeSettings();
|
2026-05-08 17:04:03 +01:00
|
|
|
pinLocale();
|
2026-05-08 03:25:16 +01:00
|
|
|
}, 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)
|
2026-05-08 03:51:48 +01:00
|
|
|
# Use a callable replacement to bypass backreference parsing in WRAPPED
|
|
|
|
|
new_html, n = pattern.subn(lambda _m: WRAPPED, html, count=1)
|
2026-05-08 03:25:16 +01:00
|
|
|
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()
|