Поддержка LRC

This commit is contained in:
Lev Plyusnin 2024-11-21 18:30:05 +07:00
parent 7bdbee96fe
commit 6dc2d0dc56
No known key found for this signature in database
GPG key ID: 21C6C2C9C0A4460D
4 changed files with 67 additions and 16 deletions

View file

@ -54,9 +54,9 @@ https://yandex-music.readthedocs.io/en/main/token.html
yandex-music-downloader --token "<Токен>" --quality 2 --url "https://music.yandex.ru/artist/208167" yandex-music-downloader --token "<Токен>" --quality 2 --url "https://music.yandex.ru/artist/208167"
``` ```
### Скачать альбом [Nevermind](https://music.yandex.ru/album/294912) в высоком качестве, загружая тексты песен ### Скачать альбом [Nevermind](https://music.yandex.ru/album/294912) в высоком качестве, загружая тексты песен в формате LRC (с временными метками)
``` ```
yandex-music-downloader --token "<Токен>" --quality 1 --add-lyrics --url "https://music.yandex.ru/album/294912" yandex-music-downloader --token "<Токен>" --quality 1 --lyrics-format lrc --url "https://music.yandex.ru/album/294912"
``` ```
### Скачать трек [Seven Nation Army](https://music.yandex.ru/album/11644078/track/6705392) ### Скачать трек [Seven Nation Army](https://music.yandex.ru/album/11644078/track/6705392)
@ -67,7 +67,8 @@ yandex-music-downloader --token "<Токен>" --url "https://music.yandex.ru/al
## Использование ## Использование
``` ```
usage: yandex-music-downloader [-h] [--quality <Качество>] [--skip-existing] usage: yandex-music-downloader [-h] [--quality <Качество>] [--skip-existing]
[--add-lyrics] [--embed-cover] [--lyrics-format {none,text,lrc}]
[--embed-cover]
[--cover-resolution <Разрешение обложки>] [--cover-resolution <Разрешение обложки>]
[--delay <Задержка>] [--stick-to-artist] [--delay <Задержка>] [--stick-to-artist]
[--only-music] [--only-music]
@ -88,7 +89,8 @@ options:
2 - Лучшее (FLAC) 2 - Лучшее (FLAC)
(по умолчанию: 0) (по умолчанию: 0)
--skip-existing Пропускать уже загруженные треки --skip-existing Пропускать уже загруженные треки
--add-lyrics Загружать тексты песен --lyrics-format {none,text,lrc}
Формат текста песни (по умолчанию: none)
--embed-cover Встраивать обложку в аудиофайл --embed-cover Встраивать обложку в аудиофайл
--cover-resolution <Разрешение обложки> --cover-resolution <Разрешение обложки>
Разрешение обложки (в пикселях). Передайте "original" для загрузки в оригинальном (наилучшем) разрешении (по умолчанию: 400) Разрешение обложки (в пикселях). Передайте "original" для загрузки в оригинальном (наилучшем) разрешении (по умолчанию: 400)

View file

@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "yandex-music-downloader" name = "yandex-music-downloader"
version = "3.2.0b4" version = "3.2.1b0"
description = "Загрузчик музыки с сервиса Яндекс.Музыка" description = "Загрузчик музыки с сервиса Яндекс.Музыка"
requires-python = ">=3.9" requires-python = ">=3.9"
readme = "README.md" readme = "README.md"

View file

@ -5,6 +5,7 @@ import logging
import re import re
import time import time
import typing import typing
from argparse import ArgumentTypeError
from collections.abc import Generator, Iterable from collections.abc import Generator, Iterable
from pathlib import Path from pathlib import Path
from typing import Optional, Union from typing import Optional, Union
@ -35,7 +36,7 @@ def quality_arg(astr: str) -> int:
aint = int(astr) aint = int(astr)
if 0 <= aint <= 2: if 0 <= aint <= 2:
return aint return aint
raise argparse.ArgumentTypeError("Значение должно быть в промежутке от 0 до 2") raise ArgumentTypeError("Значение должно быть в промежутке от 0 до 2")
def compatibility_level_arg(astr: str) -> int: def compatibility_level_arg(astr: str) -> int:
@ -44,7 +45,7 @@ def compatibility_level_arg(astr: str) -> int:
max_val = core.MAX_COMPATIBILITY_LEVEL max_val = core.MAX_COMPATIBILITY_LEVEL
if min_val <= aint <= max_val: if min_val <= aint <= max_val:
return aint return aint
raise argparse.ArgumentTypeError( raise ArgumentTypeError(
f"Значение должен быть в промежутке от {min_val} до {max_val}" f"Значение должен быть в промежутке от {min_val} до {max_val}"
) )
@ -55,6 +56,13 @@ def cover_resolution_arg(astr: str) -> int:
return int(astr) return int(astr)
def lyrics_format_arg(astr: str) -> core.LyricsFormat:
try:
return core.LyricsFormat(astr)
except ValueError:
raise ArgumentTypeError(f"Допустимые значения: {','.join(core.LyricsFormat)}")
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Загрузчик музыки с сервиса Яндекс.Музыка", description="Загрузчик музыки с сервиса Яндекс.Музыка",
@ -73,7 +81,14 @@ def main():
"--skip-existing", action="store_true", help="Пропускать уже загруженные треки" "--skip-existing", action="store_true", help="Пропускать уже загруженные треки"
) )
common_group.add_argument( common_group.add_argument(
"--add-lyrics", action="store_true", help="Загружать тексты песен" "--lyrics-format",
type=lyrics_format_arg,
default=core.LyricsFormat.NONE,
help=show_default("Формат текста песни"),
choices=core.LyricsFormat,
)
common_group.add_argument(
"--add-lyrics", action="store_true", help=argparse.SUPPRESS
) )
common_group.add_argument( common_group.add_argument(
"--embed-cover", action="store_true", help="Встраивать обложку в аудиофайл" "--embed-cover", action="store_true", help="Встраивать обложку в аудиофайл"
@ -168,6 +183,12 @@ def main():
level=logging.DEBUG if args.debug else logging.ERROR, level=logging.DEBUG if args.debug else logging.ERROR,
) )
if args.add_lyrics:
print(
"Аргумент --add-lyrics устарел и будет удален в будущем. Используйте --lyrics-format"
)
args.lyrics_format = core.LyricsFormat.TEXT
if args.url is not None: if args.url is not None:
parsed_url = urlparse(args.url) parsed_url = urlparse(args.url)
path = parsed_url.path path = parsed_url.path
@ -269,7 +290,7 @@ def main():
print(f"{format_info} Загружается {downloadable.path}") print(f"{format_info} Загружается {downloadable.path}")
core.download_track( core.download_track(
track_info=downloadable, track_info=downloadable,
add_lyrics=args.add_lyrics, lyrics_format=args.lyrics_format,
embed_cover=args.embed_cover, embed_cover=args.embed_cover,
cover_resolution=args.cover_resolution, cover_resolution=args.cover_resolution,
covers_cache=covers_cache, covers_cache=covers_cache,

View file

@ -2,9 +2,11 @@ import datetime as dt
import random import random
import re import re
import typing import typing
from collections.abc import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, StrEnum, auto
from pathlib import Path from pathlib import Path
from typing import Optional, Union from typing import Optional, Type, TypeVar, Union
import mutagen import mutagen
from mutagen.flac import FLAC, Picture from mutagen.flac import FLAC, Picture
@ -39,6 +41,25 @@ MAX_COMPATIBILITY_LEVEL = 1
AUDIO_FILE_SUFFIXES = {".mp3", ".flac", ".m4a"} AUDIO_FILE_SUFFIXES = {".mp3", ".flac", ".m4a"}
_E = TypeVar("_E", bound=Enum)
class ParameterEnum(StrEnum):
@classmethod
def available_keys(cls) -> Iterable[str]:
return cls.__members__.keys()
@classmethod
def from_str(cls: Type[_E], arg: str) -> _E:
return cls[arg.lower()]
class LyricsFormat(StrEnum):
NONE = auto()
TEXT = auto()
LRC = auto()
@dataclass @dataclass
class DownloadableTrack: class DownloadableTrack:
url: str url: str
@ -201,7 +222,7 @@ def set_tags(
def download_track( def download_track(
track_info: DownloadableTrack, track_info: DownloadableTrack,
cover_resolution: int = DEFAULT_COVER_RESOLUTION, cover_resolution: int = DEFAULT_COVER_RESOLUTION,
add_lyrics: bool = False, lyrics_format: LyricsFormat = LyricsFormat.NONE,
embed_cover: bool = False, embed_cover: bool = False,
covers_cache: Optional[dict[int, bytes]] = None, covers_cache: Optional[dict[int, bytes]] = None,
compatibility_level: int = 1, compatibility_level: int = 1,
@ -217,11 +238,18 @@ def download_track(
client.request.download(track_info.url, str(target_path)) client.request.download(track_info.url, str(target_path))
lyrics = None text_lyrics = None
if add_lyrics and (lyrics_info := track.lyrics_info): if lyrics_format != LyricsFormat.NONE and (lyrics_info := track.lyrics_info):
if lyrics_info.has_available_text_lyrics: if lyrics_format == LyricsFormat.LRC and lyrics_info.has_available_sync_lyrics:
if track_lyrics := track.get_lyrics(format="LRC"):
lrc_lyrics = track_lyrics.fetch_lyrics()
lrc_path = target_path.with_suffix(".lrc")
if not lrc_path.is_file():
with open(lrc_path, "w") as f:
f.write(lrc_lyrics)
elif lyrics_info.has_available_text_lyrics:
if track_lyrics := track.get_lyrics(format="TEXT"): if track_lyrics := track.get_lyrics(format="TEXT"):
lyrics = track_lyrics.fetch_lyrics() text_lyrics = track_lyrics.fetch_lyrics()
cover = None cover = None
if track.cover_uri is not None: if track.cover_uri is not None:
@ -242,7 +270,7 @@ def download_track(
if not cover_path.is_file(): if not cover_path.is_file():
track.download_cover(str(cover_path), cover_size) track.download_cover(str(cover_path), cover_size)
set_tags(target_path, track, lyrics, cover, compatibility_level) set_tags(target_path, track, text_lyrics, cover, compatibility_level)
def to_downloadable_track( def to_downloadable_track(