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.
84 lines
2.6 KiB
Python
Executable file
84 lines
2.6 KiB
Python
Executable file
#!/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()
|