diff --git a/database/video_database.py b/database/video_database.py index 6758df3b..7f4dd110 100644 --- a/database/video_database.py +++ b/database/video_database.py @@ -1156,10 +1156,10 @@ class VideoDatabase: return None genres = self._genres_for(conn, "movie_genres", "movie_id", movie_id) credits = self._credits_for(conn, "movie_id", movie_id) - f = conn.execute( - "SELECT resolution, quality, video_codec, audio_codec, size_bytes " - "FROM media_files WHERE movie_id=? ORDER BY size_bytes DESC LIMIT 1", - (movie_id,)).fetchone() + files = conn.execute( + "SELECT resolution, quality, video_codec, audio_codec, release_source, size_bytes " + "FROM media_files WHERE movie_id=? ORDER BY size_bytes DESC", + (movie_id,)).fetchall() finally: conn.close() return { @@ -1174,7 +1174,8 @@ class VideoDatabase: "has_poster": bool(m["poster_url"]), "has_backdrop": bool(m["backdrop_url"]), "logo": m["logo_url"], "owned": bool(m["has_file"]), "monitored": bool(m["monitored"]), - "file": (dict(f) if f else None), + "file": (dict(files[0]) if files else None), # best version (compat) + "files": [dict(x) for x in files], # all versions/editions } # ── paged/filtered/sorted library query (server-side, like music) ───────── diff --git a/tests/test_video_database.py b/tests/test_video_database.py index ae48fb7e..695ad360 100644 --- a/tests/test_video_database.py +++ b/tests/test_video_database.py @@ -312,10 +312,13 @@ def test_movie_detail_includes_owned_and_file(db): d = db.movie_detail(mid) assert d["title"] == "Dune" and d["owned"] is True and d["tmdb_id"] == 438631 assert d["file"] and d["file"]["resolution"] == "2160p" + # Full media specs surface for the owned-media block. + assert "video_codec" in d["file"] and "release_source" in d["file"] + assert d["files"] == [d["file"]] # all versions (one here) # A wishlist movie with no file reports owned False, file None. mid2 = db.upsert_movie("plex", {"server_id": "m2", "title": "Wanted"}) d2 = db.movie_detail(mid2) - assert d2["owned"] is False and d2["file"] is None + assert d2["owned"] is False and d2["file"] is None and d2["files"] == [] def test_get_art_ref_poster_vs_backdrop(db): diff --git a/webui/static/video/video-detail.js b/webui/static/video/video-detail.js index 226dff0e..4166376f 100644 --- a/webui/static/video/video-detail.js +++ b/webui/static/video/video-detail.js @@ -348,6 +348,40 @@ a.innerHTML = html; } + function mediaRes(r) { + if (!r) return ''; + r = String(r).toLowerCase(); + if (r.indexOf('2160') > -1 || r === '4k') return '4K'; + if (r.indexOf('1080') > -1) return '1080p'; + if (r.indexOf('720') > -1) return '720p'; + if (r.indexOf('480') > -1 || r.indexOf('576') > -1) return 'SD'; + return r.toUpperCase(); + } + function prettyCodec(c) { + if (!c) return ''; + var l = String(c).toLowerCase(); + if (l.indexOf('hevc') > -1 || l.indexOf('265') > -1) return 'HEVC'; + if (l.indexOf('264') > -1 || l === 'avc') return 'H.264'; + if (l.indexOf('av1') > -1) return 'AV1'; + if (l.indexOf('vp9') > -1) return 'VP9'; + return String(c).toUpperCase(); + } + function prettySource(s) { + var map = { bluray: 'Blu-ray', 'web-dl': 'WEB-DL', webdl: 'WEB-DL', webrip: 'WEBRip', + hdtv: 'HDTV', youtube: 'YouTube', dvd: 'DVD', remux: 'Remux' }; + return map[String(s || '').toLowerCase()] || String(s || ''); + } + function fmtBytes(n) { + if (!n) return ''; + var gb = n / 1073741824; + return gb >= 1 ? (Math.round(gb * 10) / 10) + ' GB' : Math.round(n / 1048576) + ' MB'; + } + function fileSummary(v) { + return [mediaRes(v.resolution), prettyCodec(v.video_codec), + v.audio_codec ? String(v.audio_codec).toUpperCase() : '', fmtBytes(v.size_bytes), + v.release_source ? prettySource(v.release_source) : ''].filter(Boolean).join(' · '); + } + function renderDetails(d) { var host = q('[data-vd-details]'); if (!host) return; @@ -357,13 +391,30 @@ if (d.studio) rows.push(['Studio', d.studio]); if (d.status) rows.push(['Status', statusLabel(d.status)]); if (d.rating_critic) rows.push(['Critic score', Math.round(d.rating_critic) + '%']); - if (d.file && d.file.resolution) rows.push(['Quality', String(d.file.resolution).toUpperCase()]); - host.innerHTML = rows.length + // Your media — the technical specs we scanned (Plex-grade). + var f = d.file; + if (f) { + if (f.resolution) rows.push(['Quality', mediaRes(f.resolution)]); + if (f.video_codec) rows.push(['Video', prettyCodec(f.video_codec)]); + if (f.audio_codec) rows.push(['Audio', String(f.audio_codec).toUpperCase()]); + if (f.release_source) rows.push(['Source', prettySource(f.release_source)]); + if (f.size_bytes) rows.push(['Size', fmtBytes(f.size_bytes)]); + } + var html = rows.length ? '