Поддержка LRC
This commit is contained in:
parent
7bdbee96fe
commit
6dc2d0dc56
4 changed files with 67 additions and 16 deletions
10
README.md
10
README.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
29
ymd/cli.py
29
ymd/cli.py
|
|
@ -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,
|
||||||
|
|
|
||||||
42
ymd/core.py
42
ymd/core.py
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue