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:
32
musicfetch
32
musicfetch
@@ -593,14 +593,18 @@ def _quality_args(quality: str) -> list[str]:
|
||||
return ["-f", "bestaudio/best", "-x"]
|
||||
|
||||
|
||||
def yt_download(url_or_query: str, target_folder: str, quality: str, dry_run: bool,
|
||||
hit: Optional[Hit] = None):
|
||||
def yt_download(url_or_query: str, target_folder: Optional[str], quality: str, dry_run: bool,
|
||||
hit: Optional[Hit] = None, outtmpl: Optional[str] = None):
|
||||
cmd = ["yt-dlp",
|
||||
*_quality_args(quality),
|
||||
"--embed-metadata",
|
||||
"--embed-thumbnail",
|
||||
"--no-playlist",
|
||||
"-P", target_folder]
|
||||
"--no-playlist"]
|
||||
# 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.
|
||||
if hit:
|
||||
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.append(url_or_query)
|
||||
|
||||
dest = outtmpl or target_folder
|
||||
if dry_run:
|
||||
print(f"[dry-run] mkdir -p {target_folder}")
|
||||
if target_folder:
|
||||
print(f"[dry-run] mkdir -p {target_folder}")
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return True
|
||||
os.makedirs(target_folder, exist_ok=True)
|
||||
print(f"Downloading via yt-dlp -> {target_folder}")
|
||||
if target_folder:
|
||||
os.makedirs(target_folder, exist_ok=True)
|
||||
print(f"Downloading via yt-dlp -> {dest}")
|
||||
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):
|
||||
url = _track_url(hit)
|
||||
artist_dir = hit.artist.split(",")[0].strip() or "Unknown Artist"
|
||||
source = hit.payload.get("extractor") or "youtube"
|
||||
target = os.path.join(root, artist_dir, source)
|
||||
return yt_download(url, target, quality, dry_run, hit=hit)
|
||||
artist_dir = hit.artist.split(",")[0].strip()
|
||||
if artist_dir:
|
||||
target = os.path.join(root, artist_dir, source)
|
||||
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):
|
||||
captured = {}
|
||||
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",
|
||||
payload={"videoId": "vid", "extractor": "youtube"})
|
||||
mf.act_youtube(h, "/media/music", "best", False)
|
||||
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 ----
|
||||
def test_download_single_bandcamp_folder(monkeypatch):
|
||||
monkeypatch.setattr(mf, "run_yt_dlp_get_metadata",
|
||||
|
||||
Reference in New Issue
Block a user