Stock Jellyfin paints .listItem.selected and .focused in cyan (#00a4dc) on actionSheet/selectionList/dialogContainer dropdowns. Clashes with Cineplex red on audio + subtitle pickers. Pick: variant 04 "Hairline ring" — 1px #E50914 outline (offset -1px inside row) + dark near-black bg + white text. Architectural, quietly on-brand. Applied via body.arrflix-themed scope. Saved alternative: web-overrides/skins/selector-variant-02-red- underline.css — variant 02 "Red underline" (matches search-input focus). Future skin/swap option per owner. Not currently active. Reconciled prod drift: prod overlay had been externally modified since last snapshot (md5 2da61583 -> c62898). Pulled prod's current overlay as new repo baseline + snapshot. Then applied variant 04 on top -> dev md5 0ab8b258 (= prod + v4). Prod still c62898 untouched.
372 lines
20 KiB
Python
Executable file
372 lines
20 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
"""Inject the ARRFLIX middle-theme v6 (logo center, Movies/Series left, search right)
|
||
into a Jellyfin web overlay's index.html. Idempotent — run repeatedly without drift.
|
||
|
||
Markers:
|
||
/* ARRFLIX-MIDDLE-THEME-BEGIN */ ... /* ARRFLIX-MIDDLE-THEME-END */ inside <style> and <script>
|
||
<!--ARRFLIX-FAVICON-BEGIN--> ... <!--ARRFLIX-FAVICON-END--> between <link> tags
|
||
|
||
Usage:
|
||
python3 bin/inject-middle-theme.py [target.html]
|
||
ARRFLIX_OVERLAY_PATH=/opt/docker/jellyfin/web-overrides/index.html python3 bin/inject-middle-theme.py
|
||
|
||
Default target: <repo_root>/web-overrides/index.html
|
||
Assets read from <repo_root>/web-overrides/assets/:
|
||
- arrflix-A.b64 favicon base64 (no data: prefix)
|
||
- arrflix-wordmark.b64-url center-logo data-URL (with data: prefix)
|
||
|
||
Doc 29 covers the design, the auth gate, and the video-page hide rule.
|
||
"""
|
||
import os, re, sys, pathlib, time
|
||
|
||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||
DEFAULT_TARGET = ROOT / "web-overrides" / "index.html"
|
||
ASSETS = ROOT / "web-overrides" / "assets"
|
||
|
||
target = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path(os.environ.get("ARRFLIX_OVERLAY_PATH", DEFAULT_TARGET))
|
||
if not target.exists():
|
||
sys.exit(f"target overlay not found: {target}")
|
||
|
||
logo_a_b64 = (ASSETS / "arrflix-A.b64").read_text(encoding="utf-8").strip()
|
||
wordmark_url = (ASSETS / "arrflix-wordmark.b64-url").read_text(encoding="utf-8").strip()
|
||
|
||
START = "/* ARRFLIX-MIDDLE-THEME-BEGIN */"
|
||
END = "/* ARRFLIX-MIDDLE-THEME-END */"
|
||
|
||
CSS = r"""
|
||
/* ===========================================================================
|
||
* ARRFLIX MIDDLE-THEME v6 — CSS layer model
|
||
* ===========================================================================
|
||
*
|
||
* STACKING ORDER (low → high) — DO NOT VIOLATE:
|
||
*
|
||
* layer 0 <html> — bg #000 (set via JS inline style; see start())
|
||
* black letterbox bars on video page come from here
|
||
* layer 1 <body> — bg #000 off-video (L1), transparent on-video (L2)
|
||
* layer 2 .backgroundContainer — Jellyfin backdrop (poster blur), bg propagated from L1/L2
|
||
* .skinBody — main app shell
|
||
* #reactRoot
|
||
* layer 3 .mainAnimatedPages — page swap container
|
||
* .pageContainer — current page
|
||
* layer 4 .skinHeader — top nav (HIDDEN during video — see :not(:has(#loginPage)))
|
||
* layer 5 .videoPlayerContainer — Jellyfin player wrapper (z:1000 by Jellyfin, fixed inset:0)
|
||
* └─ video.htmlvideoplayer — the <video> element (z:auto, inherits container stack)
|
||
* layer 6 .osdControls — Jellyfin OSD bar (scrubber, play/pause, settings)
|
||
* .videoOsdBottom — bottom controls strip
|
||
* .upNextDialog — episode-up-next overlay
|
||
* ALL Jellyfin OSD UI must stay above <video>. Jellyfin sets these
|
||
* with z-index > 1000 in stock CSS — DO NOT add a higher z-index
|
||
* to <video> or .videoPlayerContainer or you cover the controls.
|
||
* layer 7 .dialogContainer — modal dialogs (settings menu, subtitle picker)
|
||
*
|
||
* RULE: never z-index <video> or .videoPlayerContainer above 1000.
|
||
* Stock Jellyfin OSD controls float on top because their CSS sets
|
||
* z-index in the 1100–2000 range (depending on dialog vs bar).
|
||
*
|
||
* BLACK-SCREEN-OVER-VIDEO BUG CLASS — recurring (5+ times in 24h, doc 26/28/30):
|
||
* ANY rule that paints opaque bg on layer 0–4 ancestors of <video> while
|
||
* the player is mounted obscures the decoded frames. Two-layer defence:
|
||
*
|
||
* L1 (off-video): paint #000 on body+ancestors only when
|
||
* body lacks .arrflix-video-active class.
|
||
* L2 (on-video): paint transparent on every known ancestor when
|
||
* body has .arrflix-video-active. JS toggles this
|
||
* class via isVideoPage() which checks hash + DOM.
|
||
*
|
||
* SPECIFICITY NOTE: L1 (`body.arrflix-themed:not(.arrflix-video-active)`)
|
||
* and L2 (`body.arrflix-themed.arrflix-video-active`) both score (0,2,1)
|
||
* on body. Equal specificity → source order decides. L2 listed AFTER L1
|
||
* in this file → L2 wins when video-active. Good.
|
||
*
|
||
* BEFORE ADDING ANY NEW BG-COLOR RULE: ask "does this paint an ancestor of
|
||
* <video>?" If yes, scope it with `:not(.arrflix-video-active)`. Otherwise
|
||
* you reopen the black-screen bug. See doc 31 LAYER-MODEL.
|
||
* ===========================================================================
|
||
*/
|
||
|
||
/* --- HEADER LAYOUT ------------------------------------------------------ */
|
||
/* Three-column flex: nav-left | logo-center (absolute) | search-right */
|
||
body.arrflix-themed .skinHeader .headerTop{display:flex!important;align-items:center;position:relative;min-height:48px}
|
||
body.arrflix-themed .skinHeader .headerLeft,
|
||
body.arrflix-themed .skinHeader .headerRight{flex:1 1 0;display:flex;align-items:center}
|
||
body.arrflix-themed .skinHeader .headerLeft{justify-content:flex-start;gap:.4em}
|
||
body.arrflix-themed .skinHeader .headerRight{justify-content:flex-end}
|
||
|
||
/* Hide stock Jellyfin header chrome we don't want */
|
||
body.arrflix-themed .skinHeader .headerHomeButton,
|
||
body.arrflix-themed .skinHeader .pageTitleWithLogo,
|
||
body.arrflix-themed .skinHeader .headerBackButton{display:none!important}
|
||
body.arrflix-themed .skinHeader .headerLeft > h3.pageTitle:not(.pageTitleWithLogo){display:none!important}
|
||
body.arrflix-themed .skinHeader .headerCastButton,
|
||
body.arrflix-themed .skinHeader .headerSyncButton{display:none!important}
|
||
body.arrflix-themed .headerTabs.sectionTabs{display:none!important}
|
||
|
||
/* Hide 'My Media' row (.section0) — Continue Watching=section1, Next Up=section5, Recently Added=section6 unaffected */
|
||
body.arrflix-themed .homePage .homeSectionsContainer .verticalSection.section0{display:none!important}
|
||
|
||
/* Header itself disappears during video — :not(:has(#loginPage)) keeps login pre-arrflix-themed render unaffected */
|
||
body.arrflix-video-active:not(:has(#loginPage:not(.hide))) .skinHeader,
|
||
body.arrflix-video-active .arrflix-headerLogo,
|
||
body.arrflix-video-active .arrflix-nav{display:none!important}
|
||
|
||
/* Center wordmark logo — absolute pos, dead-center of headerTop */
|
||
.arrflix-headerLogo{
|
||
position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
|
||
width:120px;height:38px;
|
||
background:center/contain no-repeat url('__WORDMARK_URL__');
|
||
z-index:1;display:block;text-indent:-9999px;overflow:hidden;
|
||
}
|
||
.arrflix-headerLogo:hover{filter:brightness(1.15)}
|
||
|
||
/* Movies / Series nav links */
|
||
.arrflix-nav{
|
||
text-transform:uppercase;letter-spacing:.08em;font-weight:600;
|
||
padding:0 .9em;color:#fff!important;text-decoration:none;
|
||
display:inline-flex;align-items:center;height:100%;font-size:.85em;
|
||
transition:color .18s ease,text-shadow .18s ease,font-weight .18s ease;
|
||
}
|
||
.arrflix-nav:hover{color:#E50914!important}
|
||
/* Variant E: cinematic glow on active route — toggled by JS on hashchange */
|
||
.arrflix-nav.active{
|
||
color:#E50914!important;font-weight:700;
|
||
text-shadow:0 0 12px rgba(229,9,20,0.55),0 0 24px rgba(229,9,20,0.25);
|
||
}
|
||
|
||
/* --- L1: PURE-BLACK BG (off-video only) -------------------------------- */
|
||
/* Fires when body does NOT have .arrflix-video-active.
|
||
* Specificity (0,2,1) on body / (0,3,1) on descendants.
|
||
* <html> selector has no :not() so html stays #000 always. */
|
||
html,
|
||
body.arrflix-themed:not(.arrflix-video-active),
|
||
body.arrflix-themed:not(.arrflix-video-active) .backgroundContainer,
|
||
body.arrflix-themed:not(.arrflix-video-active) .skinBody,
|
||
body.arrflix-themed:not(.arrflix-video-active) .mainAnimatedPage,
|
||
body.arrflix-themed:not(.arrflix-video-active) .mainAnimatedPages,
|
||
body.arrflix-themed:not(.arrflix-video-active) .pageContainer,
|
||
body.arrflix-themed:not(.arrflix-video-active) #reactRoot{background-color:#000!important}
|
||
body.arrflix-themed:not(.arrflix-video-active) .backgroundContainer.withBackdrop{background-color:rgba(0,0,0,.86)!important}
|
||
|
||
/* --- L2: TRANSPARENT ANCESTORS (during video playback) ----------------- */
|
||
/* Fires when JS sets body.arrflix-video-active. Specificity matched to L1
|
||
* (0,2,1 on body, 0,3,1 on descendants). L2 wins on source order — listed
|
||
* AFTER L1 in this file. Without this, opaque ancestor bg paints over <video>.
|
||
*
|
||
* NOTE: <html> bg is pinned via JS inline style (start()) so letterbox bars
|
||
* stay BLACK even though html selector is in L1's gated rule above.
|
||
*
|
||
* Targets every known ancestor of <video.htmlvideoplayer>:
|
||
* body, .backgroundContainer, .skinBody, .mainAnimatedPage(s), .pageContainer,
|
||
* #reactRoot, .videoPlayerContainer (Jellyfin wrapper, z:1000),
|
||
* .videoPlayerContainer-onTop, #videoOsdPage + descendants, .libraryPage,
|
||
* <video> itself.
|
||
*
|
||
* DO NOT add z-index to anything in this block. OSD controls (layer 6) sit
|
||
* above <video> via Jellyfin's stock z-index 1100+. Lifting <video> z-index
|
||
* obscures controls — see image #12 incident. */
|
||
body.arrflix-themed.arrflix-video-active,
|
||
body.arrflix-themed.arrflix-video-active .backgroundContainer,
|
||
body.arrflix-themed.arrflix-video-active .skinBody,
|
||
body.arrflix-themed.arrflix-video-active .mainAnimatedPage,
|
||
body.arrflix-themed.arrflix-video-active .mainAnimatedPages,
|
||
body.arrflix-themed.arrflix-video-active .pageContainer,
|
||
body.arrflix-themed.arrflix-video-active #reactRoot,
|
||
body.arrflix-themed.arrflix-video-active .videoPlayerContainer,
|
||
body.arrflix-themed.arrflix-video-active .videoPlayerContainer-onTop,
|
||
body.arrflix-themed.arrflix-video-active #videoOsdPage,
|
||
body.arrflix-themed.arrflix-video-active #videoOsdPage .pageContainer,
|
||
body.arrflix-themed.arrflix-video-active #videoOsdPage .mainAnimatedPage,
|
||
body.arrflix-themed.arrflix-video-active #videoOsdPage .layout-desktop,
|
||
body.arrflix-themed.arrflix-video-active .libraryPage,
|
||
body.arrflix-themed.arrflix-video-active video.htmlvideoplayer{
|
||
background-color:transparent!important;
|
||
background:transparent!important;
|
||
background-image:none!important;
|
||
}
|
||
|
||
/* --- ACTION-SHEET SELECTOR (audio/subtitle dropdowns) ----------------- *
|
||
* Stock Jellyfin theme.css paints `.listItem.selected` and `.focused` in
|
||
* cyan (#00a4dc) — clashes with Cineplex red. Override to "Hairline ring"
|
||
* variant: 1px red outline (offset -1px so it sits inside the row) + dark
|
||
* near-black bg + white text. Architectural, quietly on-brand.
|
||
*
|
||
* Targets every Jellyfin picker/dropdown form:
|
||
* .actionSheet .listItem(.selected|.focused) — modal action sheets (audio/sub)
|
||
* .selectionList .listItem.selected — legacy selection lists
|
||
* .dialogContainer .listItem.selected — dialog-scoped selectors
|
||
*
|
||
* Future swap: see web-overrides/skins/selector-variant-02-red-underline.css
|
||
* for the alternative "red underline" design (matches search-input focus).
|
||
*/
|
||
body.arrflix-themed .actionSheet .listItem.selected,
|
||
body.arrflix-themed .actionSheet .listItem-button.selected,
|
||
body.arrflix-themed .actionSheet .listItem.focused,
|
||
body.arrflix-themed .selectionList .listItem.selected,
|
||
body.arrflix-themed .dialogContainer .listItem.selected{
|
||
outline:1px solid #E50914!important;
|
||
outline-offset:-1px;
|
||
background:rgba(15,15,15,.7)!important;
|
||
color:#fff!important;
|
||
}
|
||
|
||
/* --- SEARCH INPUT (cyan ring → red underline) ------------------------- */
|
||
/* Stock Jellyfin theme.css:262-272 sets blue focus ring (#00a4dc).
|
||
* Replace with borderless slab + red bottom border + soft red glow.
|
||
* Cineplex/Netflix-faithful. */
|
||
body.arrflix-themed .searchFields .emby-input,
|
||
body.arrflix-themed input.searchfields-txtSearch,
|
||
body.arrflix-themed #searchTextInput{
|
||
background:#141414!important;border:0!important;
|
||
border-bottom:2px solid transparent!important;border-radius:2px!important;
|
||
color:#fff!important;padding:.55em .8em!important;
|
||
transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;
|
||
}
|
||
body.arrflix-themed .searchFields .emby-input::placeholder,
|
||
body.arrflix-themed input.searchfields-txtSearch::placeholder,
|
||
body.arrflix-themed #searchTextInput::placeholder{color:rgba(255,255,255,.4);letter-spacing:.02em}
|
||
body.arrflix-themed .searchFields .emby-input:hover,
|
||
body.arrflix-themed input.searchfields-txtSearch:hover,
|
||
body.arrflix-themed #searchTextInput:hover{background:#1a1a1a!important}
|
||
body.arrflix-themed .searchFields .emby-input:focus,
|
||
body.arrflix-themed input.searchfields-txtSearch:focus,
|
||
body.arrflix-themed #searchTextInput:focus{
|
||
background:#1a1a1a!important;border:0!important;
|
||
border-bottom:2px solid #E50914!important;
|
||
box-shadow:0 1px 0 0 rgba(229,9,20,.35),0 0 14px -2px rgba(229,9,20,.35)!important;
|
||
outline:none!important;
|
||
}
|
||
""".replace("__WORDMARK_URL__", wordmark_url)
|
||
|
||
JS = """
|
||
/* ARRFLIX middle-theme JS shim — runtime DOM mutations + body-class toggles.
|
||
*
|
||
* BODY CLASSES we manage:
|
||
* .arrflix-themed — set when isAuthed() = true. Gates the entire theme.
|
||
* Removed on logout/login route; CSS rules disable.
|
||
* .arrflix-video-active — set when isVideoPage() = true. Gates L2 transparency
|
||
* and hides .skinHeader. Toggled live on hashchange
|
||
* + every 1.5s tick + on every body-mutation.
|
||
*
|
||
* INLINE STYLE on <html>:
|
||
* We force background-color:#000 via setProperty(...,'important') because
|
||
* getComputedStyle(html).backgroundColor inexplicably returned rgba(0,0,0,0)
|
||
* on details/video pages despite 5 stylesheet rules saying #000 !important.
|
||
* Inline style is the highest specificity short of !important user-agent.
|
||
* This guarantees the canvas behind any transparent body stays BLACK
|
||
* (so video-page letterbox bars are black, not browser-default white).
|
||
*/
|
||
(function(){
|
||
function isVideoPage(){
|
||
/* Returns true if the user is currently on a video-playback page.
|
||
* Three signals (any one is enough):
|
||
* 1. URL hash contains '/video' (Jellyfin's video route)
|
||
* 2. #videoOsdPage element is visible (the OSD page id Jellyfin mounts)
|
||
* 3. video.htmlvideoplayer (lowercase!) element is visible
|
||
* NOTE: '.htmlVideoPlayer' (camelCase) does NOT exist in Jellyfin 10.10.
|
||
* The real class is lowercase 'htmlvideoplayer'.
|
||
*/
|
||
try{
|
||
var h=(location.hash||'').toLowerCase();
|
||
if (h.indexOf('/video') !== -1) return true;
|
||
var osd = document.querySelector('#videoOsdPage:not(.hide)');
|
||
if (osd) return true;
|
||
var v = document.querySelector('video.htmlvideoplayer:not(.hide)');
|
||
if (v && getComputedStyle(v).display !== 'none') return true;
|
||
}catch(e){}
|
||
return false;
|
||
}
|
||
function isAuthed(){
|
||
try{
|
||
if (document.querySelector('.pageContainer.loginPage:not(.hide)')) return false;
|
||
if (document.querySelector('#loginPage:not(.hide)')) return false;
|
||
var h = (location.hash || '').toLowerCase();
|
||
if (h.indexOf('/login') !== -1 || h.indexOf('/wizard') !== -1 || h.indexOf('/forgotpassword') !== -1 || h.indexOf('/selectserver') !== -1) return false;
|
||
if (window.ApiClient && typeof window.ApiClient.isLoggedIn === 'function' && !window.ApiClient.isLoggedIn()) return false;
|
||
var raw = localStorage.getItem('jellyfin_credentials');
|
||
if (!raw) return false;
|
||
var creds = JSON.parse(raw);
|
||
if (!creds || !creds.Servers || !creds.Servers.length || !creds.Servers[0].AccessToken) return false;
|
||
return true;
|
||
} catch(e){ return false; }
|
||
}
|
||
function teardown(){
|
||
document.body.classList.remove('arrflix-themed');
|
||
var top = document.querySelector('.skinHeader .headerTop'); if (!top) return;
|
||
var logo = top.querySelector('.arrflix-headerLogo'); if (logo) logo.remove();
|
||
Array.prototype.forEach.call(document.querySelectorAll('.arrflix-nav'), function(n){ n.remove(); });
|
||
}
|
||
function relayoutHeader(){
|
||
document.body.classList.toggle('arrflix-video-active', isVideoPage());
|
||
if (!isAuthed()) { teardown(); return; }
|
||
var top=document.querySelector('.skinHeader .headerTop'); if(!top) return;
|
||
document.body.classList.add('arrflix-themed');
|
||
var left=top.querySelector('.headerLeft');
|
||
if(left && !left.querySelector('[data-arrflix-nav=\"movies\"]')){
|
||
left.insertAdjacentHTML('beforeend',
|
||
'<a is=\"emby-linkbutton\" class=\"emby-button arrflix-nav\" data-arrflix-nav=\"movies\" href=\"#/movies.html\">Movies</a>'+
|
||
'<a is=\"emby-linkbutton\" class=\"emby-button arrflix-nav\" data-arrflix-nav=\"series\" href=\"#/tv.html\">Series</a>'
|
||
);
|
||
}
|
||
if(!top.querySelector('.arrflix-headerLogo')){
|
||
var a=document.createElement('a');
|
||
a.className='arrflix-headerLogo';
|
||
a.href='#/home.html';
|
||
a.setAttribute('aria-label','ARRFLIX home');
|
||
a.textContent='ARRFLIX';
|
||
var right=top.querySelector('.headerRight');
|
||
top.insertBefore(a, right || null);
|
||
}
|
||
var hash=(location.hash||'').toLowerCase();
|
||
var movieMatch=(hash==='#/movies.html'||hash==='#/movies');
|
||
var seriesMatch=(hash==='#/tv.html'||hash==='#/tv');
|
||
Array.prototype.forEach.call(document.querySelectorAll('[data-arrflix-nav=\"movies\"]'),function(n){ n.classList.toggle('active',movieMatch); });
|
||
Array.prototype.forEach.call(document.querySelectorAll('[data-arrflix-nav=\"series\"]'),function(n){ n.classList.toggle('active',seriesMatch); });
|
||
}
|
||
function start(){
|
||
try{ document.documentElement.style.setProperty('background-color','#000','important'); }catch(e){}
|
||
relayoutHeader();
|
||
try{ new MutationObserver(relayoutHeader).observe(document.body,{childList:true,subtree:true}); }catch(e){}
|
||
window.addEventListener('hashchange', relayoutHeader);
|
||
setInterval(relayoutHeader,1500);
|
||
}
|
||
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',start,{once:true}); else start();
|
||
})();
|
||
"""
|
||
|
||
FAVICON_LINKS = (
|
||
"<!--ARRFLIX-FAVICON-BEGIN-->"
|
||
"<link rel=\"icon\" type=\"image/png\" sizes=\"180x180\" data-arrflix-icon=\"A\" href=\"data:image/png;base64," + logo_a_b64 + "\">"
|
||
"<link rel=\"apple-touch-icon\" sizes=\"180x180\" data-arrflix-icon=\"A\" href=\"data:image/png;base64," + logo_a_b64 + "\">"
|
||
"<!--ARRFLIX-FAVICON-END-->"
|
||
)
|
||
FAVICON_HIJACK_JS = (
|
||
"<script>/* ARRFLIX-FAVICON-HIJACK-BEGIN */"
|
||
"(function(){"
|
||
"var A_URL='data:image/png;base64," + logo_a_b64 + "';"
|
||
"function pin(){"
|
||
"Array.prototype.forEach.call(document.querySelectorAll('link[rel=\"shortcut icon\"], link[rel=\"icon\"], link[rel=\"apple-touch-icon\"]'),function(l){"
|
||
"if(l.getAttribute('data-arrflix-icon')==='A')return;"
|
||
"if((l.href||'').indexOf('data:image/png')!==-1 && l.href.length>200 && l.getAttribute('data-arrflix-icon')!=='A'){l.parentNode&&l.parentNode.removeChild(l);}"
|
||
"});"
|
||
"Array.prototype.forEach.call(document.querySelectorAll('link[data-arrflix-icon=\"A\"]'),function(l){if(l.href!==A_URL) l.href=A_URL;});"
|
||
"}"
|
||
"function start(){pin();try{new MutationObserver(pin).observe(document.head||document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:['href']});}catch(e){}setInterval(pin,1000);}"
|
||
"if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start();"
|
||
"})();"
|
||
"/* ARRFLIX-FAVICON-HIJACK-END */</script>"
|
||
)
|
||
|
||
src = target.read_text(encoding="utf-8")
|
||
src = re.sub(re.escape("<style>" + START) + r".*?" + re.escape(END + "</style>"), "", src, flags=re.DOTALL)
|
||
src = re.sub(re.escape("<script>" + START) + r".*?" + re.escape(END + "</script>"), "", src, flags=re.DOTALL)
|
||
src = re.sub(r"<!--ARRFLIX-FAVICON-BEGIN-->.*?<!--ARRFLIX-FAVICON-END-->", "", src, flags=re.DOTALL)
|
||
src = re.sub(r"<script>/\* ARRFLIX-FAVICON-HIJACK-BEGIN \*/.*?/\* ARRFLIX-FAVICON-HIJACK-END \*/</script>", "", src, flags=re.DOTALL)
|
||
|
||
PATCH = "<style>" + START + CSS + END + "</style>" + "<script>" + START + JS + END + "</script>" + FAVICON_LINKS + FAVICON_HIJACK_JS
|
||
if "</head>" not in src:
|
||
sys.exit("no </head> in target")
|
||
src2 = src.replace("</head>", PATCH + "</head>", 1)
|
||
|
||
backup = target.with_suffix(target.suffix + f".bak.pre-middle-v6.{int(time.time())}")
|
||
backup.write_text(target.read_text(encoding="utf-8"), encoding="utf-8")
|
||
target.write_text(src2, encoding="utf-8")
|
||
print(f"OK v6 wrote {len(src2)} bytes to {target}; backup at {backup}")
|