feat: download tracks with full ID3 metadata

- Add downloads.py: "Artist - Title.mp3" naming, no-overwrite dedupe,
  configurable download_dir, progress and per-item callbacks.
- Save maximum metadata via tags.py (ID3v2.3, mutagen): artists, album
  and album artist, track/disc, year/date, raw genre, label, duration,
  explicit flag, cover 600x600, plain and synced lyrics (MTS then
  LRCLIB), track URLs and Yandex/MTS ids.
- TUI/Qt: download current track and queue from the menu, "d" key in
  lists, folder picker in settings; CLI download gains -d/--dir and
  --no-tags.
- Add mutagen to requirements and document download/metadata in README.
- Fix MPRIS update_metadata crashing without dbus-next.
This commit is contained in:
jads
2026-10-25 20:06:59 +03:00
parent e71f5623a7
commit de9bf80d11
9 changed files with 754 additions and 37 deletions

View File

@@ -872,34 +872,33 @@ def cmd_play(args: argparse.Namespace) -> None:
def cmd_download(args: argparse.Namespace) -> None:
api = Api()
print_track_info(api, args.track_id)
url, _ = resolve_stream(api, args.track_id)
force_ipv4()
output = Path(args.output or f"{args.track_id}.mp3")
from downloads import download_track, stream_to_file, tag_downloaded
from settings import get_setting
with requests.get(
url,
stream=True,
headers={"User-Agent": USER_AGENT},
timeout=60,
) as response:
response.raise_for_status()
total = int(response.headers.get("Content-Length") or 0)
done = 0
with output.open("wb") as handle:
for chunk in response.iter_content(chunk_size=256 * 1024):
if not chunk:
continue
handle.write(chunk)
done += len(chunk)
if total:
percent = done * 100 // total
kib = done // 1024
print(
f"\r{percent:3d}% {kib} KiB",
end="",
file=sys.stderr)
api = Api()
track = get_track(api, args.track_id)
print(fmt_track(track), file=sys.stderr)
def progress(done: int, total: int) -> None:
if total:
percent = done * 100 // total
print(
f"\r{percent:3d}% {done // 1024} KiB",
end="",
file=sys.stderr)
with_tags = not args.no_tags
if args.output:
# Явный путь: скачиваем ровно в указанный файл, как раньше.
url, _ = resolve_stream(api, args.track_id)
output = stream_to_file(url, Path(args.output), progress)
if with_tags:
tag_downloaded(api, track, output)
else:
dest = args.dir or get_setting("download_dir")
output = download_track(api, track, dest, progress, with_tags)
if sys.stderr.isatty():
print(file=sys.stderr)
print(f"Сохранено: {output} ({output.stat().st_size} байт)")
@@ -1145,6 +1144,16 @@ def build_parser() -> argparse.ArgumentParser:
download = sub.add_parser("download", help="скачать трек в MP3")
download.add_argument("track_id")
download.add_argument("-o", "--output", help="имя файла")
download.add_argument(
"-d",
"--dir",
help="папка для сохранения (по умолчанию из download_dir)",
)
download.add_argument(
"--no-tags",
action="store_true",
help="не записывать ID3-теги (текст, обложка, метаданные)",
)
download.set_defaults(func=cmd_download)
return parser