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:
Jonathan
2026-07-11 01:01:23 +02:00
parent 00b3dddca6
commit c75b9d3562
3 changed files with 73 additions and 4 deletions
+36 -4
View File
@@ -9,6 +9,37 @@ def _hashed_password(password: str) -> str:
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:
"""Real Qobuz client backed by streamrip (v2, async → sync via asyncio.run).
@@ -76,9 +107,12 @@ class StreamripClient:
return results
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)
from streamrip.client import QobuzClient
from streamrip.db import Database, Dummy
@@ -98,8 +132,6 @@ class StreamripClient:
if album is None:
raise RuntimeError(f"could not resolve Qobuz album {album_id}")
await album.rip()
track_count = len(getattr(album, "tracks", []) or [])
on_progress(1.0)
finally:
await client.session.close()
return {"track_count": track_count, "path": dest}