Expose yt-dlp media metadata for snapshot cards

This commit is contained in:
Nick Sweeting 2026-09-01 11:49:16 -07:00
parent 4abf66c7a3
commit c2ce82964d
No known key found for this signature in database
3 changed files with 79 additions and 10 deletions

View File

@ -4628,12 +4628,26 @@ class ArchiveResult(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithNotes):
if Path(candidate).name.lower() == preferred_name:
return candidate
ext_groups = (
(".html", ".htm", ".mhtml", ".mht", ".pdf"),
(".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico"),
(".json", ".jsonl", ".txt", ".md", ".csv", ".tsv"),
(".mp4", ".webm", ".mp3", ".opus", ".ogg", ".wav"),
)
plugin_lower = (plugin_name or "").lower()
if plugin_lower in ("ytdlp", "yt-dlp", "youtube-dl"):
# yt-dlp commonly emits a thumbnail plus several media formats.
# Prefer something browsers can play directly; otherwise cards can
# select a large MKV or thumbnail that the plugin player cannot use.
ext_groups = (
(".mp4", ".webm", ".m4v", ".ogv"),
(".mp3", ".m4a", ".aac", ".opus", ".ogg", ".wav", ".flac"),
(".mkv", ".mov", ".avi", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts", ".mts", ".3gp", ".3g2"),
(".html", ".htm", ".mhtml", ".mht", ".pdf"),
(".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico"),
(".json", ".jsonl", ".txt", ".md", ".csv", ".tsv", ".srt", ".vtt"),
)
else:
ext_groups = (
(".html", ".htm", ".mhtml", ".mht", ".pdf"),
(".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico"),
(".json", ".jsonl", ".txt", ".md", ".csv", ".tsv"),
(".mp4", ".webm", ".mp3", ".opus", ".ogg", ".wav"),
)
for ext_group in ext_groups:
group_candidates = [candidate for candidate in candidates if Path(candidate).suffix.lower() in ext_group]
if group_candidates:

View File

@ -32,7 +32,7 @@ _TEXT_PREVIEW_EXTS = (".json", ".jsonl", ".txt", ".csv", ".tsv", ".xml", ".yml",
_IMAGE_PREVIEW_EXTS = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".ico", ".avif")
_STATIC_URL_SAFE = "/@-._~!$&'()*+,;="
_MEDIA_FILE_EXTS = {
_VIDEO_FILE_EXTS = {
".mp4",
".webm",
".mkv",
@ -49,6 +49,9 @@ _MEDIA_FILE_EXTS = {
".3gp",
".3g2",
".ogv",
}
_AUDIO_FILE_EXTS = {
".mp3",
".m4a",
".aac",
@ -66,6 +69,10 @@ _MEDIA_FILE_EXTS = {
".dts",
}
_MEDIA_FILE_EXTS = _VIDEO_FILE_EXTS | _AUDIO_FILE_EXTS
_BROWSER_VIDEO_FILE_EXTS = {".mp4", ".webm", ".m4v", ".ogv"}
_BROWSER_AUDIO_FILE_EXTS = {".mp3", ".m4a", ".aac", ".ogg", ".oga", ".opus", ".wav", ".flac"}
def _normalize_output_files(output_files: Any) -> dict[str, dict[str, Any]]:
if isinstance(output_files, dict):
@ -159,15 +166,27 @@ def _list_media_files(result) -> list[dict]:
for rel_path, size in candidates:
href = str(Path(result.plugin) / rel_path)
suffix = rel_path.suffix.lower()
media_type = "video" if suffix in _VIDEO_FILE_EXTS else "audio"
media_files.append(
{
"name": rel_path.name,
"path": href,
"size": size,
"media_type": media_type,
"is_video": media_type == "video",
"is_audio": media_type == "audio",
"is_browser_playable": suffix in _BROWSER_VIDEO_FILE_EXTS or suffix in _BROWSER_AUDIO_FILE_EXTS,
},
)
media_files.sort(key=lambda item: item["name"].lower())
media_files.sort(
key=lambda item: (
0 if item["is_video"] and item["is_browser_playable"] else 1 if item["is_audio"] and item["is_browser_playable"] else 2,
-int(item.get("size") or 0),
item["name"].lower(),
),
)
return media_files

View File

@ -485,10 +485,46 @@ class TestSnapshotProgressStats:
assert _count_media_files(result) == 2
assert _list_media_files(result) == [
{"name": "audio.mp3", "path": "ytdlp/audio.mp3", "size": 222},
{"name": "video.mp4", "path": "ytdlp/video.mp4", "size": 111},
{
"name": "video.mp4",
"path": "ytdlp/video.mp4",
"size": 111,
"media_type": "video",
"is_video": True,
"is_audio": False,
"is_browser_playable": True,
},
{
"name": "audio.mp3",
"path": "ytdlp/audio.mp3",
"size": 222,
"media_type": "audio",
"is_video": False,
"is_audio": True,
"is_browser_playable": True,
},
]
def test_ytdlp_discover_outputs_prefers_browser_playable_video(self, snapshot):
from archivebox.core.models import ArchiveResult
ArchiveResult.objects.create(
snapshot=snapshot,
plugin="ytdlp",
status="succeeded",
output_files={
"thumbnail.jpg": {"size": 20_000, "mimetype": "image/jpeg", "extension": "jpg"},
"large.mkv": {"size": 10_000, "mimetype": "video/x-matroska", "extension": "mkv"},
"small.mp4": {"size": 111, "mimetype": "video/mp4", "extension": "mp4"},
},
output_size=30_111,
)
outputs = snapshot.discover_outputs(include_filesystem_fallback=False)
ytdlp_output = next(output for output in outputs if output["name"] == "ytdlp")
assert ytdlp_output["path"] == "ytdlp/small.mp4"
def test_discover_outputs_falls_back_to_hashes_index_without_filesystem_walk(
self,
snapshot,