From 6ee54c12481b7dc7e95e3b04ead8cc0880789926 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 6 Oct 2026 18:24:20 +0200 Subject: [PATCH] =?UTF-8?q?Indeks:=20ffprobe-str=C3=B8mme=20og=20bitrate?= =?UTF-8?q?=20pr.=20video,=20ryd=20.playlists=20med=20tomme=20kanaler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG --- ytdl.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/ytdl.py b/ytdl.py index 6802828..c7f968c 100644 --- a/ytdl.py +++ b/ytdl.py @@ -16,6 +16,7 @@ import json import os import re import shutil +import subprocess import time import uuid import xml.etree.ElementTree as ET @@ -375,6 +376,8 @@ def _ryd_tomme_mapper(saeson_dir: str) -> None: for f in KANAL_FILER: if os.path.exists(os.path.join(kanal, f)): os.remove(os.path.join(kanal, f)) + # Playlisternes miniaturer (skrevet af opdater_playlister) hører også til kanalen. + shutil.rmtree(os.path.join(kanal, ".playlists"), ignore_errors=True) for d in os.listdir(kanal): dd = os.path.join(kanal, d) if d.startswith("Season ") and os.path.isdir(dd) and not os.listdir(dd): @@ -440,6 +443,62 @@ def _varighed_mp4(sti: str, mtime: float): return None +FFPROBE_PATH = config.DATA / "yt_ffprobe.json" +_ffprobe = {"data": None, "aendret": False} + + +def _brok(v): + try: + t, n = str(v).split("/") + return round(float(t) / float(n), 3) if float(n) else None + except (ValueError, ZeroDivisionError): + return None + + +def _heltal(v): + try: + return int(v) + except (TypeError, ValueError): + return None + + +def _stroemme(sti: str, st_) -> dict: + """Video-/lydspor (codec, opløsning, bitrate) via ffprobe, så Emby-plugin'et kan + sige til apps at filen kan afspilles direkte. Caches pr. sti+mtime+størrelse.""" + if _ffprobe["data"] is None: + _ffprobe["data"] = _load(FFPROBE_PATH, {}) or {} + noegle = f"{sti}|{int(st_.st_mtime)}|{st_.st_size}" + if noegle in _ffprobe["data"]: + return _ffprobe["data"][noegle] + ud = {"bitrate": None, "streams": []} + try: + r = subprocess.run( + [shutil.which("ffprobe") or "ffprobe", "-v", "error", "-of", "json", "-show_entries", + "stream=codec_type,codec_name,profile,width,height,pix_fmt,avg_frame_rate,bit_rate," + "channels,sample_rate:stream_disposition=attached_pic:stream_tags=language:format=bit_rate", sti], + capture_output=True, text=True, timeout=60) + d = json.loads(r.stdout or "{}") + ud["bitrate"] = _heltal((d.get("format") or {}).get("bit_rate")) + for s_ in d.get("streams", []): + if (s_.get("disposition") or {}).get("attached_pic"): + continue # det indlejrede coverbillede er ikke et videospor + if s_.get("codec_type") == "video": + ud["streams"].append({"type": "video", "codec": s_.get("codec_name"), "profile": s_.get("profile"), + "width": s_.get("width"), "height": s_.get("height"), + "fps": _brok(s_.get("avg_frame_rate")), "bitrate": _heltal(s_.get("bit_rate")), + "pix_fmt": s_.get("pix_fmt")}) + elif s_.get("codec_type") == "audio": + ud["streams"].append({"type": "audio", "codec": s_.get("codec_name"), "profile": s_.get("profile"), + "channels": s_.get("channels"), "sample_rate": _heltal(s_.get("sample_rate")), + "bitrate": _heltal(s_.get("bit_rate")), + "language": (s_.get("tags") or {}).get("language")}) + except Exception: # noqa: BLE001 + return ud # prøv igen næste gang + _ffprobe["data"][noegle] = ud + _ffprobe["aendret"] = True + return ud + + def _laes_playlister() -> dict: return _load(PLAYLISTER_PATH, {}) or {} @@ -486,7 +545,7 @@ def skriv_indeks() -> dict: "description": nfo.get("plot") or "", "upload_date": nfo.get("aired") or nfo.get("premiered") or "", "duration": _varighed_mp4(sti, st_.st_mtime), "added": int(st_.st_mtime), - "subtitles": subs} + "subtitles": subs, **_stroemme(sti, st_)} mine.append(vid) if not mine: continue @@ -517,6 +576,11 @@ def skriv_indeks() -> dict: json.dump(data, f, ensure_ascii=False, indent=1) os.chmod(tmp, 0o644) os.replace(tmp, sti) + if _ffprobe["aendret"] and _ffprobe["data"] is not None: + stier = {v["path"] for v in videoer.values()} + _ffprobe["data"] = {k: v for k, v in _ffprobe["data"].items() if k.split("|", 1)[0] in stier} + _save(FFPROBE_PATH, _ffprobe["data"]) + _ffprobe["aendret"] = False return {"channels": len(kanaler), "videos": len(videoer), "playlists": sum(len(k["playlists"]) for k in kanaler)} @@ -1092,8 +1156,9 @@ def _marked_played(ud: dict) -> bool: async def _kanal_brugerdata(c, users: list, merged: dict) -> None: """Læg set/favorit fra Emby-kanalen "YouTube" (kanal-plugin'et) oven i biblioteket for samme fil, så slettereglen og fredningen virker uanset hvor - videoen blev set. Kanalens elementer bærer video-id'et i ProviderIds["YouTube"]; - stien findes via indekset. Mangler kanalen eller indekset, sker der intet.""" + videoen blev set. Kanalens elementer bærer video-id'et i ProviderIds["youtube"]; + stien findes via indekset. Mangler kanalen eller indekset, sker der intet. + (Plugin'et synkroniserer selv kanal og bibliotek; det her er et sikkerhedsnet.)""" try: with open(indeks_sti(), encoding="utf-8") as f: stier = {vid: os.path.realpath(v["path"]) for vid, v in json.load(f)["videos"].items()}