fix: force IPv4 in worker, count real Qobuz files, flatten nested output
- worker had no IPv6 route -> Qobuz CDN (dual-stack) failed ~half the tracks with 'Network is unreachable'; disable IPv6 in the container (IPv4 only). - StreamripClient counted metadata tracks (always full) -> a partial download falsely imported; now count the actual files written (short count -> needs_attention). - streamrip writes into a nested 'Artist - Album [..]/' folder the tagger never saw; flatten audio files up into the album dir so tagging/organization runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,10 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
|
LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
|
||||||
|
# Qobuz's CDN also resolves to IPv6, but the container has no IPv6 route
|
||||||
|
# ("Network is unreachable"); disable IPv6 so downloads use IPv4 only.
|
||||||
|
sysctls:
|
||||||
|
- net.ipv6.conf.all.disable_ipv6=1
|
||||||
volumes:
|
volumes:
|
||||||
- ${MUSIC_DIR:-./music}:/music
|
- ${MUSIC_DIR:-./music}:/music
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -9,6 +9,37 @@ def _hashed_password(password: str) -> str:
|
|||||||
return hashlib.md5(password.encode("utf-8")).hexdigest()
|
return hashlib.md5(password.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
_AUDIO_EXT = {".flac", ".mp3", ".m4a", ".opus", ".ogg", ".aac", ".wav"}
|
||||||
|
|
||||||
|
|
||||||
|
def _flatten_audio(dest: str) -> int:
|
||||||
|
"""streamrip writes into a nested `Artist - Album [...]/` subfolder. Move any audio
|
||||||
|
files up into `dest`, remove the now-empty subfolders, and return the count of audio
|
||||||
|
files directly in `dest` — the REAL number downloaded, so a partial download (streamrip
|
||||||
|
continues past per-track failures) reports its true short count to the completeness check.
|
||||||
|
"""
|
||||||
|
dest = os.path.abspath(dest)
|
||||||
|
for root, _dirs, files in os.walk(dest):
|
||||||
|
if os.path.abspath(root) == dest:
|
||||||
|
continue
|
||||||
|
for name in files:
|
||||||
|
if os.path.splitext(name)[1].lower() not in _AUDIO_EXT:
|
||||||
|
continue
|
||||||
|
src = os.path.join(root, name)
|
||||||
|
target = os.path.join(dest, name)
|
||||||
|
if os.path.exists(target):
|
||||||
|
base, ext = os.path.splitext(name)
|
||||||
|
target = os.path.join(dest, f"{base} ({os.path.basename(root)}){ext}")
|
||||||
|
os.rename(src, target)
|
||||||
|
for root, _dirs, _files in os.walk(dest, topdown=False):
|
||||||
|
if os.path.abspath(root) != dest:
|
||||||
|
try:
|
||||||
|
os.rmdir(root)
|
||||||
|
except OSError:
|
||||||
|
pass # subfolder still has non-audio files (e.g. cover art) — leave it
|
||||||
|
return sum(1 for f in os.listdir(dest) if os.path.splitext(f)[1].lower() in _AUDIO_EXT)
|
||||||
|
|
||||||
|
|
||||||
class StreamripClient:
|
class StreamripClient:
|
||||||
"""Real Qobuz client backed by streamrip (v2, async → sync via asyncio.run).
|
"""Real Qobuz client backed by streamrip (v2, async → sync via asyncio.run).
|
||||||
|
|
||||||
@@ -76,9 +107,12 @@ class StreamripClient:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
def download(self, source_ref: str, dest: str, on_progress: Callable[[float], None]) -> dict:
|
def download(self, source_ref: str, dest: str, on_progress: Callable[[float], None]) -> dict:
|
||||||
return asyncio.run(self._download(source_ref, dest, on_progress))
|
asyncio.run(self._download(source_ref, dest, on_progress))
|
||||||
|
# Count the files actually written and flatten streamrip's nested output into dest
|
||||||
|
# so the tag stage can organize it. A partial download -> short count -> needs_attention.
|
||||||
|
return {"track_count": _flatten_audio(dest), "path": dest}
|
||||||
|
|
||||||
async def _download(self, album_id: str, dest: str, on_progress: Callable[[float], None]) -> dict:
|
async def _download(self, album_id: str, dest: str, on_progress: Callable[[float], None]) -> None:
|
||||||
os.makedirs(dest, exist_ok=True)
|
os.makedirs(dest, exist_ok=True)
|
||||||
from streamrip.client import QobuzClient
|
from streamrip.client import QobuzClient
|
||||||
from streamrip.db import Database, Dummy
|
from streamrip.db import Database, Dummy
|
||||||
@@ -98,8 +132,6 @@ class StreamripClient:
|
|||||||
if album is None:
|
if album is None:
|
||||||
raise RuntimeError(f"could not resolve Qobuz album {album_id}")
|
raise RuntimeError(f"could not resolve Qobuz album {album_id}")
|
||||||
await album.rip()
|
await album.rip()
|
||||||
track_count = len(getattr(album, "tracks", []) or [])
|
|
||||||
on_progress(1.0)
|
on_progress(1.0)
|
||||||
finally:
|
finally:
|
||||||
await client.session.close()
|
await client.session.close()
|
||||||
return {"track_count": track_count, "path": dest}
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from lyra_worker.adapters._streamrip import _flatten_audio
|
||||||
|
|
||||||
|
|
||||||
|
def test_flatten_moves_nested_audio_up_and_counts(tmp_path):
|
||||||
|
nested = tmp_path / "John Mayer - Continuum (2006) [FLAC] [24B-96kHz]"
|
||||||
|
nested.mkdir()
|
||||||
|
(nested / "01. Waiting.flac").write_bytes(b"x")
|
||||||
|
(nested / "02. Belief.flac").write_bytes(b"x")
|
||||||
|
(nested / "cover.jpg").write_bytes(b"x") # non-audio stays behind
|
||||||
|
|
||||||
|
count = _flatten_audio(str(tmp_path))
|
||||||
|
|
||||||
|
assert count == 2
|
||||||
|
assert (tmp_path / "01. Waiting.flac").exists()
|
||||||
|
assert (tmp_path / "02. Belief.flac").exists()
|
||||||
|
assert not (nested / "01. Waiting.flac").exists() # audio moved out of the nested folder
|
||||||
|
|
||||||
|
|
||||||
|
def test_flatten_counts_flat_files_and_ignores_non_audio(tmp_path):
|
||||||
|
(tmp_path / "a.flac").write_bytes(b"x")
|
||||||
|
(tmp_path / "b.mp3").write_bytes(b"x")
|
||||||
|
(tmp_path / "notes.txt").write_bytes(b"x")
|
||||||
|
assert _flatten_audio(str(tmp_path)) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_flatten_partial_download_reports_true_short_count(tmp_path):
|
||||||
|
# streamrip continues past per-track failures; only 3 of an album's tracks landed.
|
||||||
|
nested = tmp_path / "sub"
|
||||||
|
nested.mkdir()
|
||||||
|
for n in ["01.flac", "02.flac", "03.flac"]:
|
||||||
|
(nested / n).write_bytes(b"x")
|
||||||
|
assert _flatten_audio(str(tmp_path)) == 3 # not the album's metadata count
|
||||||
|
assert not nested.exists() # emptied subfolder removed
|
||||||
Reference in New Issue
Block a user