Очистка пути включена по умолчанию (#12), уборка кода
- Все методы from_json теперь являются методами класса (classmethod) - Импорты отсортированы с помощью isort
This commit is contained in:
parent
9e81bdbb81
commit
fb65f511b8
2 changed files with 44 additions and 39 deletions
|
|
@ -87,7 +87,7 @@ usage: main.py [-h] [--hq] [--skip-existing] [--add-lyrics] [--embed-cover]
|
||||||
[--delay <Задержка>]
|
[--delay <Задержка>]
|
||||||
[--log-level {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET,VERBOSE}]
|
[--log-level {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET,VERBOSE}]
|
||||||
(--artist-id <ID исполнителя> | --album-id <ID альбома> | --track-id <ID трека> | --playlist-id <владелец плейлиста>/<тип плейлиста> | -u URL)
|
(--artist-id <ID исполнителя> | --album-id <ID альбома> | --track-id <ID трека> | --playlist-id <владелец плейлиста>/<тип плейлиста> | -u URL)
|
||||||
[--strict-path] [--dir <Папка>] [--path-pattern <Паттерн>]
|
[--unsafe-path] [--dir <Папка>] [--path-pattern <Паттерн>]
|
||||||
--session-id <ID сессии> [--user-agent <User-Agent>]
|
--session-id <ID сессии> [--user-agent <User-Agent>]
|
||||||
|
|
||||||
Загрузчик музыки с сервиса Яндекс.Музыка
|
Загрузчик музыки с сервиса Яндекс.Музыка
|
||||||
|
|
@ -114,7 +114,7 @@ ID:
|
||||||
-u URL, --url URL URL исполнителя/альбома/трека/плейлиста
|
-u URL, --url URL URL исполнителя/альбома/трека/плейлиста
|
||||||
|
|
||||||
Указание пути:
|
Указание пути:
|
||||||
--strict-path Очищать путь от недопустимых символов
|
--unsafe-path Не очищать путь от недопустимых символов
|
||||||
--dir <Папка> Папка для загрузки музыки (по умолчанию: .)
|
--dir <Папка> Папка для загрузки музыки (по умолчанию: .)
|
||||||
--path-pattern <Паттерн>
|
--path-pattern <Паттерн>
|
||||||
Поддерживает следующие заполнители: #number, #artist,
|
Поддерживает следующие заполнители: #number, #artist,
|
||||||
|
|
|
||||||
71
main.py
71
main.py
|
|
@ -1,19 +1,20 @@
|
||||||
#!/bin/python3
|
#!/bin/python3
|
||||||
import logging
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import logging
|
||||||
import sys
|
|
||||||
import urllib.parse
|
|
||||||
import re
|
import re
|
||||||
import eyed3
|
import sys
|
||||||
from eyed3.id3.frames import ImageFrame
|
import time
|
||||||
from typing import Optional
|
import urllib.parse
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from requests import Session
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import datetime as dt
|
from typing import Optional
|
||||||
|
|
||||||
|
import eyed3
|
||||||
|
from eyed3.id3.frames import ImageFrame
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
MD5_SALT = 'XGRlBW9FXlekgbPrRHuSiA'
|
MD5_SALT = 'XGRlBW9FXlekgbPrRHuSiA'
|
||||||
ENCODED_BY = 'https://github.com/llistochek/yandex-music-downloader'
|
ENCODED_BY = 'https://github.com/llistochek/yandex-music-downloader'
|
||||||
|
|
@ -32,6 +33,7 @@ FILENAME_CLEAR_RE = re.compile(r'[^\w\-\'() ]+')
|
||||||
|
|
||||||
TITLE_FMT = '%(title)s (%(version)s)'
|
TITLE_FMT = '%(title)s (%(version)s)'
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CoverInfo:
|
class CoverInfo:
|
||||||
cover_url_template: Optional[str]
|
cover_url_template: Optional[str]
|
||||||
|
|
@ -40,11 +42,11 @@ class CoverInfo:
|
||||||
if self.cover_url_template is not None:
|
if self.cover_url_template is not None:
|
||||||
return self.cover_url_template.replace('%%', f'{resolution}x{resolution}')
|
return self.cover_url_template.replace('%%', f'{resolution}x{resolution}')
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_json(data: dict):
|
def from_json(cls, data: dict) -> 'CoverInfo':
|
||||||
if og_image := data.get('ogImage'):
|
if og_image := data.get('ogImage'):
|
||||||
return CoverInfo('https://' + og_image)
|
return cls('https://' + og_image)
|
||||||
return CoverInfo(None)
|
return cls(None)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -79,8 +81,8 @@ class BasicAlbumInfo:
|
||||||
year: Optional[int]
|
year: Optional[int]
|
||||||
artists: list[str]
|
artists: list[str]
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_json(data: dict):
|
def from_json(cls, data: dict) -> Optional['BasicAlbumInfo']:
|
||||||
if data['metaType'] != 'music':
|
if data['metaType'] != 'music':
|
||||||
logging.info('"%s" пропущен т.к. не является музыкальным альбомом', data['title'])
|
logging.info('"%s" пропущен т.к. не является музыкальным альбомом', data['title'])
|
||||||
return None
|
return None
|
||||||
|
|
@ -88,7 +90,7 @@ class BasicAlbumInfo:
|
||||||
title = parse_title(data)
|
title = parse_title(data)
|
||||||
if release_date := data.get('releaseDate'):
|
if release_date := data.get('releaseDate'):
|
||||||
release_date = dt.datetime.fromisoformat(release_date)
|
release_date = dt.datetime.fromisoformat(release_date)
|
||||||
return BasicAlbumInfo(id=data['id'], title=title, year=data.get('year'),
|
return cls(id=data['id'], title=title, year=data.get('year'),
|
||||||
artists=artists, release_date=release_date)
|
artists=artists, release_date=release_date)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -104,8 +106,8 @@ class BasicTrackInfo:
|
||||||
has_lyrics: bool
|
has_lyrics: bool
|
||||||
cover_info: CoverInfo
|
cover_info: CoverInfo
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_json(data: dict):
|
def from_json(cls, data: dict) -> Optional['BasicTrackInfo']:
|
||||||
if not data['available']:
|
if not data['available']:
|
||||||
return None
|
return None
|
||||||
track_id = str(data['id'])
|
track_id = str(data['id'])
|
||||||
|
|
@ -114,7 +116,7 @@ class BasicTrackInfo:
|
||||||
artists = parse_artists(data['artists'])
|
artists = parse_artists(data['artists'])
|
||||||
track_position = {'index': 1, 'volume': 1}
|
track_position = {'index': 1, 'volume': 1}
|
||||||
if len(albums_data) != 0:
|
if len(albums_data) != 0:
|
||||||
album_data = data['albums'][0]
|
album_data = albums_data[0]
|
||||||
album = BasicAlbumInfo.from_json(album_data)
|
album = BasicAlbumInfo.from_json(album_data)
|
||||||
track_position = album_data.get('trackPosition', track_position)
|
track_position = album_data.get('trackPosition', track_position)
|
||||||
else:
|
else:
|
||||||
|
|
@ -124,7 +126,7 @@ class BasicTrackInfo:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
cover_info = CoverInfo.from_json(data)
|
cover_info = CoverInfo.from_json(data)
|
||||||
has_lyrics = data['lyricsInfo']['hasAvailableTextLyrics']
|
has_lyrics = data['lyricsInfo']['hasAvailableTextLyrics']
|
||||||
return BasicTrackInfo(title=title, id=track_id, real_id=data['realId'],
|
return cls(title=title, id=track_id, real_id=data['realId'],
|
||||||
number=track_position['index'], disc_number=track_position['volume'],
|
number=track_position['index'], disc_number=track_position['volume'],
|
||||||
artists=artists, album=album, has_lyrics=has_lyrics, cover_info=cover_info)
|
artists=artists, album=album, has_lyrics=has_lyrics, cover_info=cover_info)
|
||||||
|
|
||||||
|
|
@ -133,25 +135,25 @@ class BasicTrackInfo:
|
||||||
class FullTrackInfo(BasicTrackInfo):
|
class FullTrackInfo(BasicTrackInfo):
|
||||||
lyrics: str
|
lyrics: str
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_json(data: dict):
|
def from_json(cls, data: dict) -> 'FullTrackInfo':
|
||||||
base = BasicTrackInfo.from_json(data['track'])
|
base = BasicTrackInfo.from_json(data['track'])
|
||||||
lyrics = data['lyric'][0]['fullLyrics']
|
lyrics = data['lyric'][0]['fullLyrics']
|
||||||
return FullTrackInfo(**base.__dict__, lyrics=lyrics)
|
return cls(**base.__dict__, lyrics=lyrics)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FullAlbumInfo(BasicAlbumInfo):
|
class FullAlbumInfo(BasicAlbumInfo):
|
||||||
tracks: list[BasicTrackInfo]
|
tracks: list[BasicTrackInfo]
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_json(data: dict):
|
def from_json(cls, data: dict) -> 'FullAlbumInfo':
|
||||||
base = BasicAlbumInfo.from_json(data)
|
base = BasicAlbumInfo.from_json(data)
|
||||||
tracks = data.get('volumes', [])
|
tracks = data.get('volumes', [])
|
||||||
tracks = [t for v in tracks for t in v]
|
tracks = [t for v in tracks for t in v]
|
||||||
tracks = map(BasicTrackInfo.from_json, tracks)
|
tracks = map(BasicTrackInfo.from_json, tracks)
|
||||||
tracks = [t for t in tracks if t is not None]
|
tracks = [t for t in tracks if t is not None]
|
||||||
return FullAlbumInfo(**base.__dict__, tracks=tracks)
|
return cls(**base.__dict__, tracks=tracks)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -161,13 +163,13 @@ class ArtistInfo:
|
||||||
albums: list[BasicAlbumInfo]
|
albums: list[BasicAlbumInfo]
|
||||||
cover_info: CoverInfo
|
cover_info: CoverInfo
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_json(data: dict):
|
def from_json(cls, data: dict) -> 'ArtistInfo':
|
||||||
artist = data['artist']
|
artist = data['artist']
|
||||||
albums = map(BasicAlbumInfo.from_json, data.get('albums', []))
|
albums = map(BasicAlbumInfo.from_json, data.get('albums', []))
|
||||||
albums = [a for a in albums if a is not None]
|
albums = [a for a in albums if a is not None]
|
||||||
cover_info = CoverInfo.from_json(data)
|
cover_info = CoverInfo.from_json(data)
|
||||||
return ArtistInfo(id=str(artist['id']), name=artist['name'],
|
return cls(id=str(artist['id']), name=artist['name'],
|
||||||
cover_info=cover_info, albums=albums)
|
cover_info=cover_info, albums=albums)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -268,7 +270,10 @@ def set_id3_tags(path: Path, track: BasicTrackInfo, lyrics: Optional[str],
|
||||||
else:
|
else:
|
||||||
release_date = track.album.year
|
release_date = track.album.year
|
||||||
audiofile = eyed3.load(path)
|
audiofile = eyed3.load(path)
|
||||||
audiofile.tag = tag = eyed3.id3.tag.Tag()
|
if audiofile is None:
|
||||||
|
raise ValueError
|
||||||
|
tag = audiofile.initTag()
|
||||||
|
|
||||||
tag.artist = chr(0).join(track.artists)
|
tag.artist = chr(0).join(track.artists)
|
||||||
tag.album_artist = track.album.artists[0]
|
tag.album_artist = track.album.artists[0]
|
||||||
tag.album = track.album.title
|
tag.album = track.album.title
|
||||||
|
|
@ -327,8 +332,8 @@ if __name__ == '__main__':
|
||||||
id_group.add_argument('-u', '--url', help='URL исполнителя/альбома/трека/плейлиста')
|
id_group.add_argument('-u', '--url', help='URL исполнителя/альбома/трека/плейлиста')
|
||||||
|
|
||||||
path_group = parser.add_argument_group('Указание пути')
|
path_group = parser.add_argument_group('Указание пути')
|
||||||
path_group.add_argument('--strict-path', action='store_true',
|
path_group.add_argument('--unsafe-path', action='store_true',
|
||||||
help='Очищать путь от недопустимых символов')
|
help='Не очищать путь от недопустимых символов')
|
||||||
path_group.add_argument('--dir', default='.', metavar='<Папка>',
|
path_group.add_argument('--dir', default='.', metavar='<Папка>',
|
||||||
help=help_str('Папка для загрузки музыки'), type=Path)
|
help=help_str('Папка для загрузки музыки'), type=Path)
|
||||||
path_group.add_argument('--path-pattern', default=DEFAULT_PATH_PATTERN,
|
path_group.add_argument('--path-pattern', default=DEFAULT_PATH_PATTERN,
|
||||||
|
|
@ -403,7 +408,7 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
for track in result_tracks:
|
for track in result_tracks:
|
||||||
album = track.album
|
album = track.album
|
||||||
save_path = args.dir / prepare_track_path(args.path_pattern, args.strict_path, track)
|
save_path = args.dir / prepare_track_path(args.path_pattern, not args.unsafe_path, track)
|
||||||
if args.skip_existing and save_path.is_file():
|
if args.skip_existing and save_path.is_file():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue