Indeks: ffprobe-strømme og bitrate pr. video, ryd .playlists med tomme kanaler

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
This commit is contained in:
root
2026-10-06 18:24:20 +02:00
parent 77f33f7c9e
commit 6ee54c1248
+68 -3
View File
@@ -16,6 +16,7 @@ import json
import os import os
import re import re
import shutil import shutil
import subprocess
import time import time
import uuid import uuid
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@@ -375,6 +376,8 @@ def _ryd_tomme_mapper(saeson_dir: str) -> None:
for f in KANAL_FILER: for f in KANAL_FILER:
if os.path.exists(os.path.join(kanal, f)): if os.path.exists(os.path.join(kanal, f)):
os.remove(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): for d in os.listdir(kanal):
dd = os.path.join(kanal, d) dd = os.path.join(kanal, d)
if d.startswith("Season ") and os.path.isdir(dd) and not os.listdir(dd): 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 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: def _laes_playlister() -> dict:
return _load(PLAYLISTER_PATH, {}) or {} return _load(PLAYLISTER_PATH, {}) or {}
@@ -486,7 +545,7 @@ def skriv_indeks() -> dict:
"description": nfo.get("plot") or "", "description": nfo.get("plot") or "",
"upload_date": nfo.get("aired") or nfo.get("premiered") or "", "upload_date": nfo.get("aired") or nfo.get("premiered") or "",
"duration": _varighed_mp4(sti, st_.st_mtime), "added": int(st_.st_mtime), "duration": _varighed_mp4(sti, st_.st_mtime), "added": int(st_.st_mtime),
"subtitles": subs} "subtitles": subs, **_stroemme(sti, st_)}
mine.append(vid) mine.append(vid)
if not mine: if not mine:
continue continue
@@ -517,6 +576,11 @@ def skriv_indeks() -> dict:
json.dump(data, f, ensure_ascii=False, indent=1) json.dump(data, f, ensure_ascii=False, indent=1)
os.chmod(tmp, 0o644) os.chmod(tmp, 0o644)
os.replace(tmp, sti) 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), return {"channels": len(kanaler), "videos": len(videoer),
"playlists": sum(len(k["playlists"]) for k in kanaler)} "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: async def _kanal_brugerdata(c, users: list, merged: dict) -> None:
"""Læg set/favorit fra Emby-kanalen "YouTube" (kanal-plugin'et) oven i """Læg set/favorit fra Emby-kanalen "YouTube" (kanal-plugin'et) oven i
biblioteket for samme fil, så slettereglen og fredningen virker uanset hvor biblioteket for samme fil, så slettereglen og fredningen virker uanset hvor
videoen blev set. Kanalens elementer bærer video-id'et i ProviderIds["YouTube"]; videoen blev set. Kanalens elementer bærer video-id'et i ProviderIds["youtube"];
stien findes via indekset. Mangler kanalen eller indekset, sker der intet.""" stien findes via indekset. Mangler kanalen eller indekset, sker der intet.
(Plugin'et synkroniserer selv kanal og bibliotek; det her er et sikkerhedsnet.)"""
try: try:
with open(indeks_sti(), encoding="utf-8") as f: 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()} stier = {vid: os.path.realpath(v["path"]) for vid, v in json.load(f)["videos"].items()}