video: owned-media tech specs on movie detail (Plex-grade)
We already scanned codec/audio/source/size but only showed resolution. The movie detail Details block now surfaces Quality / Video (HEVC, H.264…) / Audio / Source (Blu-ray, WEB-DL…) / Size, and lists every version/edition you own when there are multiple files. movie_detail now returns all media_files (not just the largest).
This commit is contained in:
parent
621693ecc8
commit
3359e3c111
4 changed files with 72 additions and 8 deletions
|
|
@ -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) ─────────
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? '<div class="vd-detail-grid">' + rows.map(function (r) {
|
||||
return '<div class="vd-detail-row"><span class="vd-detail-k">' + esc(r[0]) +
|
||||
'</span><span class="vd-detail-v">' + esc(r[1]) + '</span></div>';
|
||||
}).join('') + '</div>'
|
||||
: '';
|
||||
// Multiple versions / editions you own.
|
||||
var files = d.files || [];
|
||||
if (files.length > 1) {
|
||||
html += '<div class="vd-versions"><div class="vd-versions-h">' + files.length + ' versions</div>' +
|
||||
files.map(function (v) {
|
||||
return '<div class="vd-version">' + esc(fileSummary(v)) + '</div>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
host.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── live TMDB extras (trailer / where-to-watch / similar) ─────────────────
|
||||
|
|
|
|||
|
|
@ -1416,3 +1416,12 @@ a.vd-prov:hover img, a.vd-prov:hover .vd-prov-ph { border-color: rgba(var(--vd-a
|
|||
/* settings: detail-pages preference toggle */
|
||||
.vid-pref-row { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 14px; color: var(--text-primary, #fff); }
|
||||
.vid-pref-row input { width: 16px; height: 16px; cursor: pointer; accent-color: #38bdf8; }
|
||||
|
||||
/* ── owned media: multiple versions/editions ─────────────────────────────── */
|
||||
.vd-versions { margin-top: 18px; }
|
||||
.vd-versions-h { font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: rgba(255, 255, 255, 0.45); margin-bottom: 8px; }
|
||||
.vd-version {
|
||||
display: inline-block; margin: 0 8px 8px 0; padding: 7px 13px; border-radius: 8px;
|
||||
font-size: 13px; font-weight: 600; color: rgba(255, 255, 255, 0.85);
|
||||
background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue