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:
@@ -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()}
|
||||
|
||||
Reference in New Issue
Block a user