|
|
|
@@ -11,12 +11,16 @@ biblioteket — fx det der lå i /slow/shared/yt i forvejen — er urørligt.
|
|
|
|
|
"""
|
|
|
|
|
import asyncio
|
|
|
|
|
import fcntl
|
|
|
|
|
import functools
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
|
|
|
|
import time
|
|
|
|
|
import uuid
|
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from xml.sax.saxutils import escape
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
@@ -372,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):
|
|
|
|
@@ -395,6 +401,312 @@ def _slet_video(path: str) -> int:
|
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- indeks til Emby-kanal-plugin'et ----------------
|
|
|
|
|
# Emby kan kun vise serie → sæson → afsnit. Kanal-plugin'et (agw/emby-yt-kanaler)
|
|
|
|
|
# viser i stedet Kanal → [Alle videoer | Playlister → Playliste] → videoer, hvor
|
|
|
|
|
# en video kan ligge i flere playlister. Det læser dette indeks, som portalen
|
|
|
|
|
# skriver ud fra disken (NFO'er og billeder) og playliste-cachen. Plugin'et har
|
|
|
|
|
# ingen egen logik for YouTube; alt det ligger her.
|
|
|
|
|
INDEKS_NAVN = ".weircon-index.json"
|
|
|
|
|
PLAYLISTER_PATH = config.DATA / "yt_playlister.json"
|
|
|
|
|
PLAYLISTE_MAKS_ALDER = 7 * 86400
|
|
|
|
|
_VIDEO_ENDELSER = (".mp4", ".mkv", ".webm")
|
|
|
|
|
_SRT_RE = re.compile(r"\.([A-Za-z0-9_-]+)\.(srt|vtt)$")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def indeks_sti() -> str:
|
|
|
|
|
return os.path.join(config.YT_DOWNLOAD_DIR, INDEKS_NAVN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _laes_nfo(sti: str) -> dict:
|
|
|
|
|
"""Felter fra en NFO (episodedetails/tvshow): tag -> tekst, uniqueid pr. type."""
|
|
|
|
|
try:
|
|
|
|
|
rod = ET.parse(sti).getroot()
|
|
|
|
|
except (OSError, ET.ParseError):
|
|
|
|
|
return {}
|
|
|
|
|
ud = {}
|
|
|
|
|
for el in rod:
|
|
|
|
|
if el.tag == "uniqueid":
|
|
|
|
|
ud[f"uniqueid:{el.get('type') or ''}"] = (el.text or "").strip()
|
|
|
|
|
elif el.tag not in ud:
|
|
|
|
|
ud[el.tag] = (el.text or "").strip()
|
|
|
|
|
return ud
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@functools.lru_cache(maxsize=1024)
|
|
|
|
|
def _varighed_mp4(sti: str, mtime: float):
|
|
|
|
|
try:
|
|
|
|
|
from mutagen import File as _MFile
|
|
|
|
|
m = _MFile(sti)
|
|
|
|
|
return int(m.info.length) if m and m.info and m.info.length else None
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
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 {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def skriv_indeks() -> dict:
|
|
|
|
|
"""Skriv indekset (DOWNLOAD_DIR/.weircon-index.json) ud fra disken. Ingen netværk.
|
|
|
|
|
Returnerer en lille statistik."""
|
|
|
|
|
rod = config.YT_DOWNLOAD_DIR
|
|
|
|
|
kanaler, videoer = [], {}
|
|
|
|
|
try:
|
|
|
|
|
navne = sorted(os.listdir(rod), key=str.lower)
|
|
|
|
|
except OSError:
|
|
|
|
|
navne = []
|
|
|
|
|
for navn in navne:
|
|
|
|
|
kdir = os.path.join(rod, navn)
|
|
|
|
|
if navn.startswith(".") or not os.path.isdir(kdir):
|
|
|
|
|
continue
|
|
|
|
|
tv = _laes_nfo(os.path.join(kdir, "tvshow.nfo"))
|
|
|
|
|
kid = tv.get("uniqueid:youtube") or f"navn:{navn}"
|
|
|
|
|
mine = []
|
|
|
|
|
for d, dirs, fs in os.walk(kdir):
|
|
|
|
|
dirs[:] = [x for x in dirs if not x.startswith(".")]
|
|
|
|
|
for f in fs:
|
|
|
|
|
if not f.lower().endswith(_VIDEO_ENDELSER) or f.startswith("."):
|
|
|
|
|
continue
|
|
|
|
|
sti = os.path.join(d, f)
|
|
|
|
|
stem = f.rsplit(".", 1)[0]
|
|
|
|
|
nfo = _laes_nfo(os.path.join(d, stem + ".nfo"))
|
|
|
|
|
m = re.search(r"\[([A-Za-z0-9_-]{11})\]$", stem)
|
|
|
|
|
vid = nfo.get("uniqueid:youtube") or (m.group(1) if m else "")
|
|
|
|
|
if not vid:
|
|
|
|
|
continue
|
|
|
|
|
st_ = os.stat(sti)
|
|
|
|
|
thumb = os.path.join(d, stem + "-thumb.jpg")
|
|
|
|
|
subs = []
|
|
|
|
|
for side in sorted(fs):
|
|
|
|
|
if side.startswith(stem + "."):
|
|
|
|
|
sm = _SRT_RE.search(side[len(stem):])
|
|
|
|
|
if sm:
|
|
|
|
|
subs.append({"path": os.path.join(d, side), "lang": sm.group(1)})
|
|
|
|
|
videoer[vid] = {
|
|
|
|
|
"title": nfo.get("title") or stem, "channel": kid, "path": sti,
|
|
|
|
|
"thumb": thumb if os.path.exists(thumb) else None,
|
|
|
|
|
"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, **_stroemme(sti, st_)}
|
|
|
|
|
mine.append(vid)
|
|
|
|
|
if not mine:
|
|
|
|
|
continue
|
|
|
|
|
mine.sort(key=lambda v: (videoer[v]["upload_date"], videoer[v]["added"]), reverse=True)
|
|
|
|
|
poster, fanart = os.path.join(kdir, "poster.jpg"), os.path.join(kdir, "fanart.jpg")
|
|
|
|
|
kanaler.append({"id": kid, "name": tv.get("title") or navn, "description": tv.get("plot") or "",
|
|
|
|
|
"poster": poster if os.path.exists(poster) else None,
|
|
|
|
|
"fanart": fanart if os.path.exists(fanart) else None,
|
|
|
|
|
"videos": mine, "playlists": [], "_dir": kdir})
|
|
|
|
|
|
|
|
|
|
# Playlister fra cachen; kun hentede videoer, i YouTubes rækkefølge.
|
|
|
|
|
cache = _laes_playlister()
|
|
|
|
|
for k in kanaler:
|
|
|
|
|
for pl in (cache.get(k["id"]) or {}).get("playlists", []):
|
|
|
|
|
vids = [v for v in pl.get("videos", []) if v in videoer]
|
|
|
|
|
if not vids:
|
|
|
|
|
continue
|
|
|
|
|
thumb = os.path.join(k["_dir"], ".playlists", f"{pl['id']}.jpg")
|
|
|
|
|
k["playlists"].append({"id": pl["id"], "title": pl.get("title") or pl["id"],
|
|
|
|
|
"thumb": thumb if os.path.exists(thumb) else None, "videos": vids})
|
|
|
|
|
k.pop("_dir", None)
|
|
|
|
|
|
|
|
|
|
data = {"version": 1, "generated": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
|
|
|
"channels": kanaler, "videos": videoer}
|
|
|
|
|
sti = indeks_sti()
|
|
|
|
|
tmp = sti + ".tmp"
|
|
|
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
|
|
|
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)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _ytdlp_json(url: str) -> dict:
|
|
|
|
|
"""yt-dlp --flat-playlist -J med samme nye forsøg som hentning (403/429)."""
|
|
|
|
|
err = ""
|
|
|
|
|
for pause, _ in FORSOEG:
|
|
|
|
|
if pause:
|
|
|
|
|
await asyncio.sleep(pause)
|
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
|
|
|
config.YT_DLP_BIN, "--flat-playlist", "-J", "--no-warnings", "--ignore-errors", url,
|
|
|
|
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
|
|
|
out, e = await proc.communicate()
|
|
|
|
|
try:
|
|
|
|
|
return json.loads(out.decode("utf-8", "replace") or "{}")
|
|
|
|
|
except ValueError:
|
|
|
|
|
err = e.decode("utf-8", "replace")
|
|
|
|
|
if not midlertidig(err):
|
|
|
|
|
break
|
|
|
|
|
raise RuntimeError((err.strip().splitlines() or ["ukendt fejl"])[-1][:200])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stoerste_thumb(thumbs: list) -> str:
|
|
|
|
|
gode = [t for t in thumbs or [] if t.get("url")]
|
|
|
|
|
gode.sort(key=lambda t: (t.get("width") or 0) * (t.get("height") or 0), reverse=True)
|
|
|
|
|
return gode[0]["url"] if gode else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _hent_playliste(pl_url: str) -> dict:
|
|
|
|
|
d = await _ytdlp_json(pl_url)
|
|
|
|
|
return {"id": d.get("id") or "", "title": d.get("title") or "",
|
|
|
|
|
"channel_id": d.get("channel_id") or d.get("uploader_id") or "",
|
|
|
|
|
"thumb_url": _stoerste_thumb(d.get("thumbnails")),
|
|
|
|
|
"videos": [e["id"] for e in (d.get("entries") or []) if e and e.get("id")]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def opdater_playlister(kun_nye: bool = False) -> dict:
|
|
|
|
|
"""Hent kanalernes playlister (og deres video-id'er) til cachen.
|
|
|
|
|
|
|
|
|
|
Hver kanal opdateres højst hver 7. dag; kun_nye=True tager kun kanaler uden
|
|
|
|
|
cache. Portalens egne playliste-job og -abonnementer tages altid med og
|
|
|
|
|
knyttes til playlistens ejer-kanal. Bagefter skrives indekset.
|
|
|
|
|
"""
|
|
|
|
|
res = {"kanaler": 0, "playlister": 0, "fejl": []}
|
|
|
|
|
cache = _laes_playlister()
|
|
|
|
|
rod = config.YT_DOWNLOAD_DIR
|
|
|
|
|
kanal_dirs = {}
|
|
|
|
|
for navn in sorted(os.listdir(rod)):
|
|
|
|
|
kdir = os.path.join(rod, navn)
|
|
|
|
|
if navn.startswith(".") or not os.path.isdir(kdir):
|
|
|
|
|
continue
|
|
|
|
|
kid = _laes_nfo(os.path.join(kdir, "tvshow.nfo")).get("uniqueid:youtube")
|
|
|
|
|
if kid:
|
|
|
|
|
kanal_dirs[kid] = kdir
|
|
|
|
|
nu = time.time()
|
|
|
|
|
|
|
|
|
|
for kid in kanal_dirs:
|
|
|
|
|
gl = cache.get(kid) or {}
|
|
|
|
|
if kun_nye and gl:
|
|
|
|
|
continue
|
|
|
|
|
if gl and nu - gl.get("updated", 0) < PLAYLISTE_MAKS_ALDER:
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
d = await _ytdlp_json(f"https://www.youtube.com/channel/{kid}/playlists")
|
|
|
|
|
pls = []
|
|
|
|
|
for e in d.get("entries") or []:
|
|
|
|
|
if not e or not e.get("id"):
|
|
|
|
|
continue
|
|
|
|
|
await asyncio.sleep(2) # skån YouTube
|
|
|
|
|
try:
|
|
|
|
|
pls.append(await _hent_playliste(
|
|
|
|
|
e.get("url") or f"https://www.youtube.com/playlist?list={e['id']}"))
|
|
|
|
|
except Exception as ex: # noqa: BLE001
|
|
|
|
|
res["fejl"].append(f"{e.get('title') or e['id']}: {ex}")
|
|
|
|
|
# Bevar egne playlister (fra job/abonnementer) der ikke står på kanalens fane.
|
|
|
|
|
egne = [p for p in gl.get("playlists", []) if p.get("egen")]
|
|
|
|
|
ids = {p["id"] for p in pls}
|
|
|
|
|
cache[kid] = {"updated": nu, "playlists": pls + [p for p in egne if p["id"] not in ids]}
|
|
|
|
|
res["kanaler"] += 1
|
|
|
|
|
except Exception as ex: # noqa: BLE001
|
|
|
|
|
res["fejl"].append(f"{kid}: {ex}")
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
|
|
|
|
|
# Portalens egne playlister (job med list= og playliste-abonnementer).
|
|
|
|
|
st = get_state()
|
|
|
|
|
urls = {j["url"] for j in st["jobs"].values() if "list=" in (j.get("url") or "")}
|
|
|
|
|
urls |= {s["url"] for s in st["subs"].values() if s.get("kind") == "playlist"}
|
|
|
|
|
kendte = {p["id"] for c in cache.values() for p in c.get("playlists", [])}
|
|
|
|
|
for url in sorted(urls):
|
|
|
|
|
m = re.search(r"list=([A-Za-z0-9_-]+)", url)
|
|
|
|
|
if m and m.group(1) in kendte:
|
|
|
|
|
# Allerede i en kanals cache; markér den som egen, så den bevares.
|
|
|
|
|
for c in cache.values():
|
|
|
|
|
for p in c.get("playlists", []):
|
|
|
|
|
if p["id"] == m.group(1):
|
|
|
|
|
p["egen"] = True
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
pl = await _hent_playliste(
|
|
|
|
|
f"https://www.youtube.com/playlist?list={m.group(1)}" if m else url)
|
|
|
|
|
pl["egen"] = True
|
|
|
|
|
ejer = pl.get("channel_id") or ""
|
|
|
|
|
c = cache.setdefault(ejer, {"updated": 0, "playlists": []})
|
|
|
|
|
c["playlists"] = [p for p in c["playlists"] if p["id"] != pl["id"]] + [pl]
|
|
|
|
|
except Exception as ex: # noqa: BLE001
|
|
|
|
|
res["fejl"].append(f"{url}: {ex}")
|
|
|
|
|
|
|
|
|
|
# Miniaturer til <Kanal>/.playlists/<id>.jpg (kun når de mangler).
|
|
|
|
|
for kid, c in cache.items():
|
|
|
|
|
kdir = kanal_dirs.get(kid)
|
|
|
|
|
if not kdir:
|
|
|
|
|
continue
|
|
|
|
|
for p in c.get("playlists", []):
|
|
|
|
|
mappe = os.path.join(kdir, ".playlists")
|
|
|
|
|
sti = os.path.join(mappe, f"{p['id']}.jpg")
|
|
|
|
|
if p.get("thumb_url") and not os.path.exists(sti):
|
|
|
|
|
os.makedirs(mappe, exist_ok=True)
|
|
|
|
|
await _hent_billede(p["thumb_url"], sti)
|
|
|
|
|
res["playlister"] += 1
|
|
|
|
|
_save(PLAYLISTER_PATH, cache)
|
|
|
|
|
skriv_indeks()
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Fejl der ofte går over af sig selv: YouTube afviser en enkelt forespørgsel
|
|
|
|
|
# (403/429), eller forbindelsen falder. yt-dlp gentager ikke selv en 403.
|
|
|
|
|
_MIDLERTIDIG = ("403", "forbidden", "429", "too many requests", "timed out",
|
|
|
|
@@ -841,35 +1153,79 @@ def _marked_played(ud: dict) -> bool:
|
|
|
|
|
return bool((ud or {}).get("Played"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _kanal_brugerdata(c, users: list, merged: dict, navn: str) -> bool:
|
|
|
|
|
"""Læg set/favorit fra Emby-kanalen (kanal-plugin'et) ind i `merged`, nøglet på
|
|
|
|
|
filens sti. Kanalens elementer bærer video-id'et i ProviderIds["youtube"]; stien
|
|
|
|
|
findes via indekset. Samme video ligger i "Alle videoer" og i playlister, og
|
|
|
|
|
kopierne deler brugerdata, så det er ligegyldigt hvilken vi læser.
|
|
|
|
|
|
|
|
|
|
False hvis kanalen eller indekset mangler. Fejler opslaget for en bruger, kastes
|
|
|
|
|
der: uden alle brugeres favoritter må slettereglen ikke køre."""
|
|
|
|
|
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()}
|
|
|
|
|
except (OSError, ValueError, KeyError):
|
|
|
|
|
return False
|
|
|
|
|
if not users:
|
|
|
|
|
return False
|
|
|
|
|
kanaler = await _emby(c, "/Channels", UserId=users[0])
|
|
|
|
|
kanal = next((k for k in (kanaler.get("Items") or [])
|
|
|
|
|
if (k.get("Name") or "").lower() == navn.lower()), None)
|
|
|
|
|
if not kanal:
|
|
|
|
|
return False
|
|
|
|
|
for uid in users:
|
|
|
|
|
data = await _emby(c, f"/Users/{uid}/Items", ParentId=kanal["Id"],
|
|
|
|
|
Recursive="true", IncludeItemTypes="Video", Fields="ProviderIds,UserData")
|
|
|
|
|
for it in data.get("Items", []):
|
|
|
|
|
pid = it.get("ProviderIds") or {}
|
|
|
|
|
vid = pid.get("youtube") or pid.get("YouTube") or pid.get("Youtube")
|
|
|
|
|
sti = stier.get(vid or "")
|
|
|
|
|
if not sti:
|
|
|
|
|
continue
|
|
|
|
|
m = merged.setdefault(sti, {"id": it["Id"], "name": it.get("Name") or os.path.basename(sti),
|
|
|
|
|
"path": sti, "played": False, "favorite": False})
|
|
|
|
|
ud = it.get("UserData") or {}
|
|
|
|
|
if _marked_played(ud):
|
|
|
|
|
m["played"] = True
|
|
|
|
|
if ud.get("IsFavorite"):
|
|
|
|
|
m["favorite"] = True
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def emby_yt_items() -> dict:
|
|
|
|
|
"""Alle videoer i YouTube-biblioteket med set/favorit aggregeret over ALLE brugere."""
|
|
|
|
|
"""Alle videoer i Emby med set/favorit aggregeret over ALLE brugere.
|
|
|
|
|
|
|
|
|
|
Kilden er kanalen fra kanal-plugin'et og/eller et bibliotek med samme navn
|
|
|
|
|
(standard "YouTube"). Findes ingen af dem, er svaret ok=False, så intet slettes."""
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
navn = settings.get("library_name") or "YouTube"
|
|
|
|
|
out = {"ok": False, "items": [], "error": ""}
|
|
|
|
|
try:
|
|
|
|
|
async with httpx.AsyncClient(timeout=45) as c:
|
|
|
|
|
lib = await _library_id(c, settings.get("library_name") or "YouTube")
|
|
|
|
|
if not lib:
|
|
|
|
|
out["error"] = (f"Fandt ikke Emby-biblioteket "
|
|
|
|
|
f"'{settings.get('library_name')}'")
|
|
|
|
|
return out
|
|
|
|
|
users = [u["Id"] for u in await _emby(c, "/Users")]
|
|
|
|
|
merged = {}
|
|
|
|
|
for uid in users:
|
|
|
|
|
data = await _emby(c, f"/Users/{uid}/Items", ParentId=lib,
|
|
|
|
|
Recursive="true", Fields="Path,UserData")
|
|
|
|
|
for it in data.get("Items", []):
|
|
|
|
|
path = it.get("Path") or ""
|
|
|
|
|
if not path:
|
|
|
|
|
continue
|
|
|
|
|
key = os.path.realpath(path)
|
|
|
|
|
m = merged.setdefault(key, {
|
|
|
|
|
"id": it["Id"], "name": it.get("Name") or os.path.basename(path),
|
|
|
|
|
"path": key, "played": False, "favorite": False})
|
|
|
|
|
ud = it.get("UserData") or {}
|
|
|
|
|
if _marked_played(ud):
|
|
|
|
|
m["played"] = True
|
|
|
|
|
if ud.get("IsFavorite"):
|
|
|
|
|
m["favorite"] = True
|
|
|
|
|
lib = await _library_id(c, navn)
|
|
|
|
|
if lib:
|
|
|
|
|
for uid in users:
|
|
|
|
|
data = await _emby(c, f"/Users/{uid}/Items", ParentId=lib,
|
|
|
|
|
Recursive="true", Fields="Path,UserData")
|
|
|
|
|
for it in data.get("Items", []):
|
|
|
|
|
path = it.get("Path") or ""
|
|
|
|
|
if not path:
|
|
|
|
|
continue
|
|
|
|
|
key = os.path.realpath(path)
|
|
|
|
|
m = merged.setdefault(key, {
|
|
|
|
|
"id": it["Id"], "name": it.get("Name") or os.path.basename(path),
|
|
|
|
|
"path": key, "played": False, "favorite": False})
|
|
|
|
|
ud = it.get("UserData") or {}
|
|
|
|
|
if _marked_played(ud):
|
|
|
|
|
m["played"] = True
|
|
|
|
|
if ud.get("IsFavorite"):
|
|
|
|
|
m["favorite"] = True
|
|
|
|
|
kanal = await _kanal_brugerdata(c, users, merged, navn)
|
|
|
|
|
if not lib and not kanal:
|
|
|
|
|
out["error"] = f"Fandt hverken Emby-kanalen eller -biblioteket '{navn}'"
|
|
|
|
|
return out
|
|
|
|
|
out["items"] = list(merged.values())
|
|
|
|
|
out["ok"] = True
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
@@ -878,6 +1234,11 @@ async def emby_yt_items() -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def refresh_library():
|
|
|
|
|
# Kanal-plugin'ets indeks følger altid disken: efter hentning og sletning.
|
|
|
|
|
try:
|
|
|
|
|
skriv_indeks()
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
async with httpx.AsyncClient(timeout=30) as c:
|
|
|
|
|
await c.post(f"{config.EMBY_URL}/emby/Library/Refresh",
|
|
|
|
|