Fix viewing-history artwork and add transcode playback metrics
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import sqlite3
|
||||
import time
|
||||
@@ -12,10 +13,50 @@ from ..clients.jellyfin import JellyfinClient
|
||||
from ..clients.jellystat import JellystatClient, JellystatError
|
||||
from ..runtime import get_runtime_settings
|
||||
from .jellyfin_identity import link_user, linked_user_id
|
||||
from .insights_artwork import item_id as artwork_item_id, with_artwork
|
||||
|
||||
_cache: dict[tuple, tuple[float, dict]] = {}
|
||||
CACHE_SECONDS = 60
|
||||
|
||||
HARDWARE = {"amf": "AMD AMF", "qsv": "Intel Quick Sync", "nvenc": "NVIDIA NVENC",
|
||||
"v4l2m2m": "V4L2", "vaapi": "VAAPI", "videotoolbox": "Apple VideoToolbox", "rkmpp": "Rockchip MPP"}
|
||||
HARDWARE_ENUM = {0: "none", 1: "amf", 2: "qsv", 3: "nvenc", 4: "v4l2m2m", 5: "vaapi", 6: "videotoolbox", 7: "rkmpp"}
|
||||
|
||||
|
||||
def add_transcoding(row, duration, media_type, totals, hardware, audio_codecs):
|
||||
# Jellystat can retain stale transcoding metadata after a switch to DirectPlay.
|
||||
method = row.get("PlayMethod")
|
||||
if method not in {"Transcode", "DirectStream"}:
|
||||
return
|
||||
info = row.get("TranscodingInfo")
|
||||
if isinstance(info, str):
|
||||
try:
|
||||
info = json.loads(info)
|
||||
except ValueError:
|
||||
info = None
|
||||
info = info if isinstance(info, dict) else {}
|
||||
video_present = media_type in {"movie", "episode"} or bool(info.get("VideoCodec"))
|
||||
if method == "Transcode" and video_present:
|
||||
if info.get("IsVideoDirect") is False:
|
||||
totals["video_minutes"] += duration
|
||||
value = info.get("HardwareAccelerationType")
|
||||
value = HARDWARE_ENUM.get(value) if type(value) is int else str(value or "").strip().lower()
|
||||
if value in HARDWARE:
|
||||
totals["hardware_video_minutes"] += duration
|
||||
hardware[HARDWARE[value]] += duration
|
||||
elif value == "none":
|
||||
totals["software_video_minutes"] += duration
|
||||
else:
|
||||
totals["unknown_hardware_minutes"] += duration
|
||||
elif info.get("IsVideoDirect") is not True:
|
||||
totals["unknown_video_minutes"] += duration
|
||||
if info.get("IsAudioDirect") is False:
|
||||
totals["audio_minutes"] += duration
|
||||
codec = str(info.get("AudioCodec") or "Unknown").upper()[:30]
|
||||
audio_codecs[codec] += duration
|
||||
elif info.get("IsAudioDirect") is not True:
|
||||
totals["unknown_audio_minutes"] += duration
|
||||
|
||||
|
||||
def _date(value) -> datetime:
|
||||
try:
|
||||
@@ -85,6 +126,9 @@ def summarize(history: list, libraries: list, start: datetime, end: datetime) ->
|
||||
daily_seconds = defaultdict(float)
|
||||
clients = defaultdict(float)
|
||||
methods = defaultdict(float)
|
||||
transcoding = dict.fromkeys(("video_minutes", "audio_minutes", "hardware_video_minutes", "software_video_minutes",
|
||||
"unknown_hardware_minutes", "unknown_video_minutes", "unknown_audio_minutes"), 0.0)
|
||||
hardware, audio_codecs = defaultdict(float), defaultdict(float)
|
||||
titles = {}
|
||||
movie_ids, episode_ids, seen = set(), set(), set()
|
||||
recent = []
|
||||
@@ -107,6 +151,7 @@ def summarize(history: list, libraries: list, start: datetime, end: datetime) ->
|
||||
episode_id = row.get("EpisodeId")
|
||||
library_type = library_types.get(str(row.get("ParentId")), "")
|
||||
media_type = "episode" if episode_id else "movie" if library_type == "movies" else "other"
|
||||
add_transcoding(row, duration / 60, media_type, transcoding, hardware, audio_codecs)
|
||||
if media_type == "episode":
|
||||
episode_ids.add(str(episode_id))
|
||||
elif media_type == "movie":
|
||||
@@ -126,7 +171,7 @@ def summarize(history: list, libraries: list, start: datetime, end: datetime) ->
|
||||
recent.append({"id": row_id, "title": name, "series": series, "type": media_type,
|
||||
"episode": f"S{row.get('SeasonNumber', '?')} · E{row.get('EpisodeNumber', '?')}" if episode_id else None,
|
||||
"minutes": round(duration / 60, 1), "played_at": date.isoformat(), "client": client,
|
||||
"method": method})
|
||||
"method": method, "artwork_item_id": artwork_item_id(row.get("NowPlayingItemId"))})
|
||||
count = (end.date() - start.date()).days + 1
|
||||
daily = [{"date": (start.date() + timedelta(days=i)).isoformat(),
|
||||
"minutes": round(daily_seconds.get((start.date() + timedelta(days=i)).isoformat(), 0) / 60, 2)} for i in range(count)]
|
||||
@@ -149,6 +194,10 @@ def summarize(history: list, libraries: list, start: datetime, end: datetime) ->
|
||||
"daily": daily, "top_titles": top,
|
||||
"clients": [{"name": name, "minutes": round(value / 60, 1)} for name, value in sorted(clients.items(), key=lambda pair: -pair[1])[:6]],
|
||||
"methods": [{"name": name, "minutes": round(value / 60, 1)} for name, value in sorted(methods.items(), key=lambda pair: -pair[1])],
|
||||
"transcoding": {**{name: round(value, 1) for name, value in transcoding.items()},
|
||||
"hardware": [{"name": name, "minutes": round(value, 1)} for name, value in sorted(hardware.items(), key=lambda pair: -pair[1])],
|
||||
"audio_codecs": [{"name": name, "minutes": round(value, 1)} for name, value in sorted(audio_codecs.items(), key=lambda pair: -pair[1])],
|
||||
"gpu_busy_minutes": None},
|
||||
"recent": sorted(recent, key=lambda row: row["played_at"], reverse=True)[:20]}
|
||||
|
||||
|
||||
@@ -169,7 +218,7 @@ async def get_insights(user: dict, days: int) -> dict:
|
||||
runtime.jellyfin_base_url, identity, days)
|
||||
cached = _cache.get(key)
|
||||
if cached and cached[0] > time.monotonic():
|
||||
return {**base, **cached[1]}
|
||||
return {**base, **with_artwork(cached[1], user, runtime)}
|
||||
history, libraries = await client.get_user_history(identity, start, end)
|
||||
data = {**summarize(history, libraries, start, end), "state": "ready", "updated_at": end.isoformat(),
|
||||
"period_start": start.isoformat(), "period_end": end.isoformat()}
|
||||
@@ -178,4 +227,4 @@ async def get_insights(user: dict, days: int) -> dict:
|
||||
if len(_cache) >= 128:
|
||||
_cache.pop(next(iter(_cache)))
|
||||
_cache[key] = (time.monotonic() + CACHE_SECONDS, data)
|
||||
return {**base, **data}
|
||||
return {**base, **with_artwork(data, user, runtime)}
|
||||
|
||||
Reference in New Issue
Block a user