fix: route sparse-metadata playlist tracks by yt-dlp's own metadata
SoundCloud sets (and similar) return flat-playlist entries without per-track artist/title. When a track Hit has no artist, download via an output template (-o <root>/%(artist,uploader,channel)s/<source>/...) so yt-dlp places the file under the real artist instead of "Unknown Artist". yt_download gains an optional outtmpl mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
24
musicfetch
24
musicfetch
@@ -593,14 +593,18 @@ def _quality_args(quality: str) -> list[str]:
|
|||||||
return ["-f", "bestaudio/best", "-x"]
|
return ["-f", "bestaudio/best", "-x"]
|
||||||
|
|
||||||
|
|
||||||
def yt_download(url_or_query: str, target_folder: str, quality: str, dry_run: bool,
|
def yt_download(url_or_query: str, target_folder: Optional[str], quality: str, dry_run: bool,
|
||||||
hit: Optional[Hit] = None):
|
hit: Optional[Hit] = None, outtmpl: Optional[str] = None):
|
||||||
cmd = ["yt-dlp",
|
cmd = ["yt-dlp",
|
||||||
*_quality_args(quality),
|
*_quality_args(quality),
|
||||||
"--embed-metadata",
|
"--embed-metadata",
|
||||||
"--embed-thumbnail",
|
"--embed-thumbnail",
|
||||||
"--no-playlist",
|
"--no-playlist"]
|
||||||
"-P", target_folder]
|
# Either a fixed output dir (-P) or a metadata-driven output template (-o).
|
||||||
|
if outtmpl:
|
||||||
|
cmd += ["-o", outtmpl]
|
||||||
|
else:
|
||||||
|
cmd += ["-P", target_folder]
|
||||||
# Override tags from the chosen hit so they don't rely on scraped titles.
|
# Override tags from the chosen hit so they don't rely on scraped titles.
|
||||||
if hit:
|
if hit:
|
||||||
if hit.artist:
|
if hit.artist:
|
||||||
@@ -616,12 +620,15 @@ def yt_download(url_or_query: str, target_folder: str, quality: str, dry_run: bo
|
|||||||
cmd += ["--parse-metadata", f"{hit.year}:%(release_year)s"]
|
cmd += ["--parse-metadata", f"{hit.year}:%(release_year)s"]
|
||||||
cmd.append(url_or_query)
|
cmd.append(url_or_query)
|
||||||
|
|
||||||
|
dest = outtmpl or target_folder
|
||||||
if dry_run:
|
if dry_run:
|
||||||
|
if target_folder:
|
||||||
print(f"[dry-run] mkdir -p {target_folder}")
|
print(f"[dry-run] mkdir -p {target_folder}")
|
||||||
print(f"[dry-run] {' '.join(cmd)}")
|
print(f"[dry-run] {' '.join(cmd)}")
|
||||||
return True
|
return True
|
||||||
|
if target_folder:
|
||||||
os.makedirs(target_folder, exist_ok=True)
|
os.makedirs(target_folder, exist_ok=True)
|
||||||
print(f"Downloading via yt-dlp -> {target_folder}")
|
print(f"Downloading via yt-dlp -> {dest}")
|
||||||
return subprocess.run(cmd).returncode == 0
|
return subprocess.run(cmd).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
@@ -648,10 +655,15 @@ def _track_url(hit: Hit) -> str:
|
|||||||
|
|
||||||
def act_youtube(hit: Hit, root: str, quality: str, dry_run: bool):
|
def act_youtube(hit: Hit, root: str, quality: str, dry_run: bool):
|
||||||
url = _track_url(hit)
|
url = _track_url(hit)
|
||||||
artist_dir = hit.artist.split(",")[0].strip() or "Unknown Artist"
|
|
||||||
source = hit.payload.get("extractor") or "youtube"
|
source = hit.payload.get("extractor") or "youtube"
|
||||||
|
artist_dir = hit.artist.split(",")[0].strip()
|
||||||
|
if artist_dir:
|
||||||
target = os.path.join(root, artist_dir, source)
|
target = os.path.join(root, artist_dir, source)
|
||||||
return yt_download(url, target, quality, dry_run, hit=hit)
|
return yt_download(url, target, quality, dry_run, hit=hit)
|
||||||
|
# Sparse playlist metadata (e.g. SoundCloud sets): let yt-dlp route the file
|
||||||
|
# by the track's own metadata so it lands under the real artist.
|
||||||
|
outtmpl = os.path.join(root, "%(artist,uploader,channel)s", source, "%(title)s [%(id)s].%(ext)s")
|
||||||
|
return yt_download(url, None, quality, dry_run, hit=hit, outtmpl=outtmpl)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -60,13 +60,27 @@ def test_act_youtube_soundcloud_folder(monkeypatch):
|
|||||||
def test_act_youtube_youtube_folder(monkeypatch):
|
def test_act_youtube_youtube_folder(monkeypatch):
|
||||||
captured = {}
|
captured = {}
|
||||||
monkeypatch.setattr(mf, "yt_download",
|
monkeypatch.setattr(mf, "yt_download",
|
||||||
lambda url, target, quality, dry_run, hit=None: captured.update(target=target) or True)
|
lambda url, target, quality, dry_run, hit=None, outtmpl=None:
|
||||||
|
captured.update(target=target) or True)
|
||||||
h = mf.Hit(source="youtube", kind="track", title="T", artist="A",
|
h = mf.Hit(source="youtube", kind="track", title="T", artist="A",
|
||||||
payload={"videoId": "vid", "extractor": "youtube"})
|
payload={"videoId": "vid", "extractor": "youtube"})
|
||||||
mf.act_youtube(h, "/media/music", "best", False)
|
mf.act_youtube(h, "/media/music", "best", False)
|
||||||
assert captured["target"] == "/media/music/A/youtube"
|
assert captured["target"] == "/media/music/A/youtube"
|
||||||
|
|
||||||
|
|
||||||
|
def test_act_youtube_unknown_artist_uses_metadata_template(monkeypatch):
|
||||||
|
captured = {}
|
||||||
|
monkeypatch.setattr(mf, "yt_download",
|
||||||
|
lambda url, target, quality, dry_run, hit=None, outtmpl=None:
|
||||||
|
captured.update(target=target, outtmpl=outtmpl) or True)
|
||||||
|
h = mf.Hit(source="youtube", kind="track", title="", artist="",
|
||||||
|
payload={"videoId": None, "extractor": "soundcloud", "url": "https://soundcloud.com/a/t"})
|
||||||
|
mf.act_youtube(h, "/media/music", "best", False)
|
||||||
|
assert captured["target"] is None
|
||||||
|
assert "%(artist,uploader,channel)s" in captured["outtmpl"]
|
||||||
|
assert captured["outtmpl"].endswith("/soundcloud/%(title)s [%(id)s].%(ext)s")
|
||||||
|
|
||||||
|
|
||||||
# ---- download_single per-source folder ----
|
# ---- download_single per-source folder ----
|
||||||
def test_download_single_bandcamp_folder(monkeypatch):
|
def test_download_single_bandcamp_folder(monkeypatch):
|
||||||
monkeypatch.setattr(mf, "run_yt_dlp_get_metadata",
|
monkeypatch.setattr(mf, "run_yt_dlp_get_metadata",
|
||||||
|
|||||||
Reference in New Issue
Block a user