bin: add set-home-layout.py — disable Next Up, ensure Continue Watching
Some checks are pending
secret-scan / gitleaks (HEAD + history) (push) Waiting to run
secret-scan / detect-secrets (entropy + cross-tool) (push) Waiting to run
secret-scan / summary (push) Blocked by required conditions

Applied 2026-05-11 across both prod (13 users) and dev (1 user).
Replaces 'nextup' homesection slot with 'none' on every user; seeds 'resume'
at slot 0 for users whose CustomPrefs was empty (would have fallen back to
the Jellyfin factory default that includes Next Up).

Idempotent — re-running is a no-op once converged.
This commit is contained in:
s8n 2026-05-11 03:01:42 +01:00
parent a6ce8451fa
commit e686cc07e0

84
bin/set-home-layout.py Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Patch Jellyfin home-screen section layout for every user on a given instance.
Default policy (this script):
- Continue Watching (`resume`) ENABLED for every user
- Resume Audio (`resumeaudio`) preserved if present
- Next Up (`nextup`) DISABLED (replaced with `none`)
- Latest Media (`latestmedia`) preserved if present
- If a user's layout was empty (factory default), seed slot 0 with `resume`
Idempotent. Safe to re-run.
Usage:
JF_URL=https://arrflix.s8n.ru \
JF_TOKEN=<admin token> \
python3 bin/set-home-layout.py
Token retrieval (admin):
docker cp jellyfin:/config/data/jellyfin.db /tmp/jf.db
sqlite3 /tmp/jf.db \\
'SELECT d.AccessToken FROM Devices d JOIN Users u ON d.UserId=u.Id
WHERE u.Username="s8n" ORDER BY d.DateLastActivity DESC LIMIT 1'
"""
import json
import os
import sys
import urllib.request
JF_URL = os.environ.get("JF_URL")
JF_TOKEN = os.environ.get("JF_TOKEN")
if not JF_URL or not JF_TOKEN:
sys.exit("set JF_URL and JF_TOKEN env vars")
def http(method, url, body=None):
req = urllib.request.Request(url, method=method)
req.add_header("X-Emby-Token", JF_TOKEN)
data = None
if body is not None:
req.add_header("Content-Type", "application/json")
data = json.dumps(body).encode()
with urllib.request.urlopen(req, data=data) as r:
text = r.read().decode()
return r.status, (json.loads(text) if text else None)
def patch_user(user_id, username):
url = f"{JF_URL}/DisplayPreferences/usersettings?userId={user_id}&client=emby"
_, prefs = http("GET", url)
cp = prefs.get("CustomPrefs") or {}
sections = [cp.get(f"homesection{i}", "none") for i in range(10)]
before = list(sections)
sections = ["none" if s == "nextup" else s for s in sections]
if "resume" not in sections:
if "none" in sections:
sections[sections.index("none")] = "resume"
else:
sections = ["resume"] + sections[:9]
for i, s in enumerate(sections):
cp[f"homesection{i}"] = s
prefs["CustomPrefs"] = cp
changed = before != sections
if changed:
http("POST", url, body=prefs)
return changed, before, sections
def main():
users = http("GET", f"{JF_URL}/Users")[1]
print(f"{JF_URL}{len(users)} users")
for u in users:
changed, before, after = patch_user(u["Id"], u["Name"])
mark = "CHANGED" if changed else " ok "
print(f" [{mark}] {u['Name']:<14} {before} -> {after}")
if __name__ == "__main__":
main()