From e686cc07e0a8512e27ae628f3294fc26fbb74c2c Mon Sep 17 00:00:00 2001 From: s8n Date: Mon, 11 May 2026 03:01:42 +0100 Subject: [PATCH] =?UTF-8?q?bin:=20add=20set-home-layout.py=20=E2=80=94=20d?= =?UTF-8?q?isable=20Next=20Up,=20ensure=20Continue=20Watching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- bin/set-home-layout.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100755 bin/set-home-layout.py diff --git a/bin/set-home-layout.py b/bin/set-home-layout.py new file mode 100755 index 0000000..afa3ff3 --- /dev/null +++ b/bin/set-home-layout.py @@ -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= \ + 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()