feat(yt): overvåg playlister og kanaler, med cycle X på kanaler
Playliste-abonnement henter nye videoer efterhånden som de lægges i playlisten. Kanal-abonnement holder en rullende buffer: cycle X = de X nyeste videoer ligger klar, og kommer der en ny, ryger den ældste ud. X tæller kun kanal-abonnementets egne hentninger. Manuelle hentninger, playliste-abonnementer og favoritter tæller ikke med — og favoritter slettes aldrig, så en favoritering frigør en plads i cyklussen til næste video. Tre ting der måtte designes udenom: 1. `seen` husker hvert video-id abonnementet nogensinde har hentet, også efter videoen er slettet igen. Uden det ville set-reglen slette en video efter 7 dage, hvorefter kanalen stadig har den blandt sine nyeste og henter den igen — i en uendelig løkke. 2. Playliste og kanal primer forskelligt ved oprettelse. En playliste kan have hundredvis af videoer, og ønsket var NYE videoer, så første tjek markerer det eksisterende som kendt uden at hente. En kanal henter derimod de X nyeste med det samme; det er hele pointen med cycle X. Resten af bagkataloget markeres som kendt, så den aldrig begynder at hente gammelt indhold. 3. enforce_cycles rører intet hvis Emby ikke svarer. Uden favorit-data kunne den komme til at slette noget fredet — samme konservative holdning som mediaclean. Downloads gemmer nu video-id og upload-dato, så cyklussen kan sortere efter hvor ny videoen ER, ikke hvornår vi tilfældigvis hentede den. Timeren kører i rækkefølgen: tjek abonnementer -> hent -> håndhæv cyklus -> slet sete. Cyklussen skal håndhæves efter hentningen, ellers ser den et halvt bibliotek. Claude-Session: https://claude.ai/code/session_011WxG8pbLj61jYU1MMaD7VC
This commit is contained in:
@@ -536,7 +536,7 @@ async def admin_yt(request: Request, saved: str = ""):
|
||||
"settings": yt.get_settings(), "human": yt._human, "now": time.time(),
|
||||
"download_dir": config.YT_DOWNLOAD_DIR, "unconfigured": False,
|
||||
"ours": [], "ours_size": 0, "leaving": st["leaving"], "log": st["log"][:15],
|
||||
"jobs": jobs[:25], "inv_error": "",
|
||||
"jobs": jobs[:25], "inv_error": "", "subs": [],
|
||||
"queued": sum(1 for j in jobs if j["status"] == "queued"),
|
||||
"running": any(j["status"] == "running" for j in jobs)}
|
||||
if not yt.configured():
|
||||
@@ -554,6 +554,16 @@ async def admin_yt(request: Request, saved: str = ""):
|
||||
ours.sort(key=lambda v: v["name"].lower())
|
||||
ctx["ours"] = ours
|
||||
ctx["ours_size"] = sum(v["size"] for v in ours)
|
||||
favs = {it["path"] for it in inv.get("items", []) if it["favorite"]}
|
||||
subs = []
|
||||
for sb in st["subs"].values():
|
||||
files = yt.sub_files(st, sb["id"])
|
||||
subs.append({**sb,
|
||||
"on_disk": len(files),
|
||||
"in_cycle": sum(1 for f in files if f["path"] not in favs),
|
||||
"favorites": sum(1 for f in files if f["path"] in favs)})
|
||||
subs.sort(key=lambda x: (x["kind"], x["title"].lower()))
|
||||
ctx["subs"] = subs
|
||||
return templates.TemplateResponse(request, "admin_yt.html", ctx)
|
||||
|
||||
|
||||
@@ -605,3 +615,74 @@ async def admin_yt_run(request: Request):
|
||||
msg = (f"Kørt — {res['seen']} af vores videoer · {len(res['new'])} nye på vej ud · "
|
||||
f"{len(res['spared'])} fredet · {len(res['deleted'])} slettet")
|
||||
return RedirectResponse(f"/admin/yt?saved={msg}", status_code=303)
|
||||
|
||||
|
||||
@app.post("/admin/yt/sub/add")
|
||||
async def admin_yt_sub_add(request: Request, url: str = Form(""), kind: str = Form(""),
|
||||
cycle: str = Form("")):
|
||||
require_admin(request)
|
||||
url = (url or "").strip()
|
||||
if not url.startswith(("http://", "https://")):
|
||||
return RedirectResponse("/admin/yt?saved=Afvist: det ligner ikke en URL", status_code=303)
|
||||
kind = kind if kind in ("playlist", "channel") else yt.sub_kind_from_url(url)
|
||||
try:
|
||||
n = int(cycle or 0)
|
||||
except ValueError:
|
||||
n = 0
|
||||
if kind == "channel" and n < 1:
|
||||
return RedirectResponse(
|
||||
"/admin/yt?saved=Afvist: en kanal skal have et cycle-tal (hvor mange nye videoer du vil have liggende)",
|
||||
status_code=303)
|
||||
sub = await yt.add_sub(url, kind=kind, cycle=n if kind == "channel" else 0)
|
||||
asyncio.create_task(_yt_subs_run())
|
||||
cache.clear("yt:")
|
||||
return RedirectResponse(
|
||||
f"/admin/yt?saved=Overvåger nu {sub['title']} — henter i baggrunden", status_code=303)
|
||||
|
||||
|
||||
@app.post("/admin/yt/sub/{sid}/save")
|
||||
async def admin_yt_sub_save(request: Request, sid: str, cycle: str = Form(""),
|
||||
enabled: str = Form("")):
|
||||
require_admin(request)
|
||||
patch = {"enabled": bool(enabled)}
|
||||
try:
|
||||
if cycle != "":
|
||||
patch["cycle"] = max(0, int(cycle))
|
||||
except ValueError:
|
||||
pass
|
||||
yt.set_sub(sid, **patch)
|
||||
cache.clear("yt:")
|
||||
return RedirectResponse("/admin/yt?saved=Abonnement gemt", status_code=303)
|
||||
|
||||
|
||||
@app.post("/admin/yt/sub/{sid}/del")
|
||||
async def admin_yt_sub_del(request: Request, sid: str):
|
||||
require_admin(request)
|
||||
yt.del_sub(sid)
|
||||
cache.clear("yt:")
|
||||
return RedirectResponse(
|
||||
"/admin/yt?saved=Abonnement fjernet — allerede hentede videoer bliver liggende",
|
||||
status_code=303)
|
||||
|
||||
|
||||
async def _yt_subs_run():
|
||||
"""Tjek abonnementer, hent det nye, og hold kanalernes cyklus."""
|
||||
try:
|
||||
await yt.sync_subs()
|
||||
await yt.process_queue()
|
||||
await yt.enforce_cycles()
|
||||
cache.clear("yt:")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@app.post("/admin/yt/subs/check")
|
||||
async def admin_yt_subs_check(request: Request):
|
||||
require_admin(request)
|
||||
res = await yt.sync_subs()
|
||||
asyncio.create_task(_yt_subs_run())
|
||||
cache.clear("yt:")
|
||||
msg = f"Tjekkede {res['checked']} abonnementer — {res['queued']} nye videoer i kø"
|
||||
if res["errors"]:
|
||||
msg += f" · {len(res['errors'])} fejl: {res['errors'][0][:80]}"
|
||||
return RedirectResponse(f"/admin/yt?saved={msg}", status_code=303)
|
||||
|
||||
Reference in New Issue
Block a user