From e0bfb2e8ed420f53893c56b5fa1faf85bfd959d7 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 9 Aug 2026 23:32:12 +0200 Subject: [PATCH] fix: use build_catalog.sh as single source of truth for catalog --- __pycache__/app.cpython-310.pyc | Bin 13169 -> 12142 bytes app.py | 104 ++++++++++---------------------- 2 files changed, 33 insertions(+), 71 deletions(-) diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc index 2e0d351c91286bb0cd21bc49a4355307f4b64b60..201b183fbc1944fcb5a1bcb2e9d9fe9a2ca284dc 100644 GIT binary patch delta 4626 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 S)rEt;28=>1xEPNSVgCcleQTQl delta 5650 zcmbtYdvsLA8NYM)?%kW+&1MOCLS6y^mOuicAQ15ZzKWuNFA!aq%uTXkv%Ac_0f{#* zHEFADvC^^q!&=X#4|~*l)Y6x&+Q+fh9((NRIX%^0TPv;AYOB>_k3C}5{=T^z5`>=q z(ao9Po%!aQnQvyk*W``s-s(>js;i?Keiy%T!%%4Dp+pP&?V+x(u*O5r>?rX$6WWgX ze6Ahs*Le$XoiKJR;BCAe)K0sl+`u~~89#;3o6yGDj)kBs;`1k&UBefQ6Y9X^H1-?3 z(@yY(D2wcNzSwT>w|I&#nFwR_Vl}!8^DVVgSZx{ao+ugUSb~l&-h+-m?^ykeIqx-M?sBWTcKyj7>Ty2Pz^+~N9 zn$$dvvtG?LhQh8n6hTYpA#NND@h~?J>P+JrCXH3-bKJ0rqQ zXPmxl);F_xZs%RG5S?O?RsxIVTQtJW4%@y#H9zbYcxJy}m30dJ1u>j)MGEYGbf3t& zcG|UXa77mp!hB@7=%kn*>I2Vwno_|6>Wn*?y>=QMnOtFDB`A&osAX+TtVN{{Mbi$y zP_>pN*P-T{j+=2uoRlus$;Gjq>|FVY*ja3~{8_A%Z8`k6*jIFR!QtByF~%;Jch+pJ zPKA8Ed%3tCV?{>3Thq?2lg-I><`qP^Qtn8uUDr=llIUJt4B+Fl8^qugO^}pg2yi`$ z5>7wRpoy!9e6{>la%ImoRK1vBF2E1v?EJk1p6X|ka0lq+WO(pKu-N$f^8kJF>& zv8NUy6PH@nkGlmSa1ZIhj5D|qWQQDqr6*XjF3xH#F^XnzM5xfO8YPCZ0mXS4sxqLB zK|#uFQWGz^x*IARt~SX^<7}gL?Qh4Jt9fj$Hl}-eISiH-kCjc%CYfm6Q_XdcW!YHB zgARem@C-H0ZJsd{;YOfQWouHyOxKKsy>Pi|Qlqa}&m&%VFB50&ITh3>k8o>J->Hw8 zV-e3JF0T}!k6t11s=b=n?nS(iXZC6sWb){4T?~1VatuP*ZrqEEvpgdX9<6Z1z;Ppw z^Xdr=d{Lg5#FoYxIARr!8gLvOwcKhN4SG$lb`@r48q;7<+fm(fDo;ZChA|6!oaI@K z8unS`RjGa6!)ra}S^av0hQYIYF)Yva-0miDRyDkCl8v*ms23G4jMlqJFUsqigXJ4~ zHQtDkiZV2-GQNox&o>#GTf6@c>}qW+#+H+AgJK!6MV(>}cahE8j+h`qSP}e4 z*5SA-SCA|6!~MAe9Ic?bF_0_lQf|UG@|j^zi$ zEecL&WtqXccgj~A7O_X1ZnE)J1YX23^IQQR!ow5&5}Q{oOe+OqfzvN9!vAbawS zU4=qU*%EG_Ox_O_Mqnhv6}YR2N6GRLZF`0$$^%lB17Aag-w?b& zKx<%SrEibRo7x*%UjoIc1z0fS4wyKZ{{m&~;zjvzducWqocK7w=Lik}XdD8ZCpo!6mr^JgHob#{bt5Zq!cw{ zCBqFP3oyWdoQP{9O!RnRq&NoBlMdJP@J3-+#*K_?JaQK$`_|@@hEmFk#d8F|Bk&1G zC&cpv)7LnJ=6gi?Jz!KmG_ONm-xe+%v$0fohGrjsz__Cx8adPORcQDlQIFlcAA!fo zxQHz(WGHSkP)y}vrf>gx8kZqhuLM-0E+maH8d*Fn*{WxM(Is*vp9Cil_-VfUi}b1*yXLX zmCq1-n&7hpbmp>tVREN9h}!?HbsY?IDy?f%hZE#W^6X@t(!kqjNje|AGZR7Iu^Ur| zUUSmR`W6j4OwdYj{M`ka%d~o)ppeRWs2kk!RT@66ng&d|ke<@Zb!koQhpj8Q+Q87(eL-DRzk`3Dj0UvcVH{^7F*0cJy=9BI5hh zrYPRgzL4auLwic@go?DuAlCi^ezjFzviP+6Khcms6ObPcvZKF{x13F$awW||9-h3V zcvJplac9Crlgb7Z=SBI?#n+XLa)gHiRlRnFdiX`SImtW??=9}>aCUgbsVAT&wxShj z*2)&QJgq?k;yE=*JQ46z;hBgnpjCBJo+`M?m2Op_@_6^?ZggC$=h3@D@RqTeT7SPR zrs_*7;s(*0u79+W6jC;?QZe|82;S4kgSoHcwv_T!JjGiWa6+;ZFbBG(ykHw~ouF&v z{QOWpVebp>C(t$AOx>Bm41v2_Md!N^yp|p~xhS+Lfj<+Pl~8W41=ymTA9AF34$oV% zlrc|U+BLV^*s4>7~sN(-?JL`V4Q$0-xB3Q&kVK%#;S1 z_#cIMU)C;LVfE9>Ol+b2`!c_Z7t-8Ds=dFTcqs~-y7+*2di_W`%?o{q>R+JM+6lS{ zehR?jXJl7V6#8t(5qpVBaZx_FtbdL=gD8>IL8Pd*k>Env)7@C2#38Uh71EB~o_ZDq zo4F4xzCk`fJVM-*(ul{1Xa)(Xc!DTT5j;!qBEb=WA6D^9yh4Q62wo@n2LaiXAF3;k zxZ)jZtKj@T)jpuZkbmp0VM4ywJ>Rm7M71@~S}qs#ENiWX65yY_)~ptmj9Z~PX4OGm J>QRgY`wtsmPAmWb diff --git a/app.py b/app.py index 7128d45..468d8e1 100644 --- a/app.py +++ b/app.py @@ -25,7 +25,7 @@ from pydantic import BaseModel APP_DIR = Path(os.environ.get("APP_DIR", "/data/app")) MOVIES_DIR = APP_DIR / "movies" -CATALOG = APP_DIR / "catalog.json" +CATALOG = MOVIES_DIR / "catalog.json" WEBHOOK = os.environ.get("DEPLOY_WEBHOOK", "") WEBHOOK_TOKEN = os.environ.get("COOLIFY_TOKEN", "") @@ -48,10 +48,6 @@ def load_catalog(): return {"movies": []} return json.loads(CATALOG.read_text(encoding="utf-8")) -def save_catalog(cat): - cat["updated"] = datetime.date.today().isoformat() - CATALOG.write_text(json.dumps(cat, ensure_ascii=False, indent=2), encoding="utf-8") - def deploy(): if not WEBHOOK: return {"deployed": False, "reason": "no webhook configured"} @@ -100,50 +96,18 @@ def torrent_hash(path: Path): return hashlib.sha1(enc(info)).hexdigest() def rebuild_catalog(): - """Rebuild catalog.json from /movies folders (port of build_catalog.sh).""" - movies = [] - if MOVIES_DIR.exists(): - for entry in sorted(os.listdir(MOVIES_DIR)): - d = MOVIES_DIR / entry - if not d.is_dir() or entry.startswith("."): - continue - meta = {"id": entry} - mf = d / "meta.txt" - if mf.exists(): - for line in mf.read_text(encoding="utf-8", errors="replace").splitlines(): - line = line.strip() - if not line or ":" not in line: - continue - k, v = line.split(":", 1) - k, v = k.strip(), v.strip() - if k in ("title", "original_title", "description"): - meta[k] = v - elif k == "year": - meta["year"] = int(v) if v.isdigit() else v - elif k == "genre": - meta["genre"] = [g.strip() for g in v.split(",") if g.strip()] - elif k == "rating": - try: meta["rating"] = float(v) - except: pass - # poster - for ext in ("jpg", "png", "jpeg", "webp"): - p = d / ("poster." + ext) - if p.exists(): - meta["poster"] = "movies/%s/poster.%s" % (entry, ext) - break - # torrent - tors = list(d.glob("*.torrent")) - if tors: - try: - meta["hash"] = torrent_hash(tors[0]) - meta["torrent"] = "movies/%s/%s" % (entry, tors[0].name) - except Exception as e: - meta["torrent_error"] = str(e) - movies.append(meta) + """Rebuild catalog.json by running the project's build_catalog.sh + (keeps a single source of truth for catalog generation).""" + script = MOVIES_DIR.parent / "build_catalog.sh" + if not script.exists(): + raise HTTPException(500, "build_catalog.sh not found at %s" % script) + try: + subprocess.run(["bash", str(script)], check=True, + capture_output=True, text=True, timeout=120) + except subprocess.CalledProcessError as e: + raise HTTPException(500, "build failed: " + (e.stderr or e.stdout or str(e))) cat = load_catalog() - cat["movies"] = movies - save_catalog(cat) - return len(movies) + return len(cat.get("movies", [])) # ---------- endpoints ---------- @@ -247,16 +211,27 @@ class PatchBody(BaseModel): @app.patch("/movie/{fid}") def patch_movie(fid: str, body: PatchBody): fid = valid_id(fid) - cat = load_catalog() - m = next((x for x in cat.get("movies", []) if x.get("id") == fid), None) - if not m: + d = MOVIES_DIR / fid + if not d.exists(): raise HTTPException(404, "no such film") + mf = d / "meta.txt" + # update meta.txt fields (source of truth for build_catalog.sh) + fields = {} + if mf.exists(): + for line in mf.read_text(encoding="utf-8", errors="replace").splitlines(): + line = line.strip() + if not line or ":" not in line: + continue + k, v = line.split(":", 1) + fields[k.strip()] = v.strip() for k in ("title", "year", "description", "poster", "original_title"): v = getattr(body, k) if v is not None: - m[k] = v - save_catalog(cat) - return {"updated": fid, "deploy": deploy()} + fields[k] = str(v) + mf.write_text("\n".join("%s: %s" % (k, v) for k, v in fields.items()) + "\n", + encoding="utf-8") + n = rebuild_catalog() + return {"updated": fid, "catalog_movies": n, "deploy": deploy()} @app.post("/movie/{fid}/meta") def post_meta(fid: str, meta: UploadFile = File(None)): @@ -283,13 +258,8 @@ def post_poster(fid: str, poster: UploadFile = File(...)): if ext not in ("jpg", "jpeg", "png", "webp"): raise HTTPException(400, "bad poster ext") (d / ("poster." + ext)).write_bytes(poster.file.read()) - # update catalog poster ref - cat = load_catalog() - m = next((x for x in cat.get("movies", []) if x.get("id") == fid), None) - if m: - m["poster"] = "movies/%s/poster.%s" % (fid, ext) - save_catalog(cat) - return {"poster_updated": fid, "deploy": deploy()} + n = rebuild_catalog() + return {"poster_updated": fid, "catalog_movies": n, "deploy": deploy()} @app.post("/movie/{fid}/torrent") def post_torrent(fid: str, torrent: UploadFile = File(...)): @@ -317,16 +287,8 @@ def rename_movie(fid: str, body: RenameBody): if (MOVIES_DIR / new).exists(): raise HTTPException(409, "target %s exists" % new) d.rename(MOVIES_DIR / new) - cat = load_catalog() - for m in cat.get("movies", []): - if m.get("id") == fid: - m["id"] = new - if "poster" in m: - m["poster"] = m["poster"].replace("movies/%s/" % fid, "movies/%s/" % new) - if "torrent" in m: - m["torrent"] = m["torrent"].replace("movies/%s/" % fid, "movies/%s/" % new) - save_catalog(cat) - return {"renamed": fid, "to": new, "deploy": deploy()} + n = rebuild_catalog() + return {"renamed": fid, "to": new, "catalog_movies": n, "deploy": deploy()} @app.get("/check") def check():