diff --git a/.gitignore b/.gitignore index c8dc31a..80ff238 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,5 @@ cython_debug/ #.idea/ config.toml +.python-version +.vscode/* diff --git a/config.example.toml b/config.example.toml index a16e963..d20b0e8 100644 --- a/config.example.toml +++ b/config.example.toml @@ -73,10 +73,10 @@ afterDownloaded = "" # title, artist, album, album_artist, composer, # genre, created, track, tracknum, disk, # record_company, upc, isrc, copyright, -# lyrics, cover +# lyrics, cover, ratings(rtng) embedMetadata = ["title", "artist", "album", "album_artist", "composer", "genre", "created", "track", "tracknum", "disk", "lyrics", "cover", "copyright", - "record_company", "upc", "isrc"] + "record_company", "upc", "isrc","rtng"] [mitm] # The host proxy server listens on diff --git a/src/api.py b/src/api.py index d146318..12bfdc7 100644 --- a/src/api.py +++ b/src/api.py @@ -159,7 +159,7 @@ async def get_cover(url: str, cover_format: str, cover_size: str): async def get_song_info(song_id: str, token: str, storefront: str, lang: str): async with request_lock: req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{song_id}", - params={"extend": "extendedAssetUrls", "include": "albums", "l": lang}, + params={"extend": "extendedAssetUrls", "include": "albums,explicit", "l": lang}, headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_itunes, "Origin": "https://music.apple.com"}) song_data_obj = SongData.model_validate(req.json()) diff --git a/src/metadata.py b/src/metadata.py index 0f90b0a..16b3770 100644 --- a/src/metadata.py +++ b/src/metadata.py @@ -41,6 +41,12 @@ class SongMetadata(BaseModel): lrc = ttml_convent_to_lrc(value) tags.append(f"{key}={lrc}") continue + if key.lower() in ('upc', 'isrc'): + tags.append(f"WM/{key.lower()}={value}") + continue + if key == 'composer': + tags.append(f"writer={value}") + continue tags.append(f"{key}={value}") return ":".join(tags) @@ -55,7 +61,8 @@ class SongMetadata(BaseModel): copyright=song_data.relationships.albums.data[0].attributes.copyright, record_company=song_data.relationships.albums.data[0].attributes.recordLabel, upc=song_data.relationships.albums.data[0].attributes.upc, - isrc=song_data.attributes.isrc + isrc=song_data.attributes.isrc, + rtng=1 if song_data.attributes.contentRating and song_data.attributes.contentRating == 'explicit' else 0 ) def set_lyrics(self, lyrics: str): diff --git a/src/models/album_meta.py b/src/models/album_meta.py index 818c210..08ed970 100644 --- a/src/models/album_meta.py +++ b/src/models/album_meta.py @@ -113,7 +113,7 @@ class Datum1(BaseModel): type: Optional[str] = None href: Optional[str] = None attributes: Attributes1 - relationships: Relationships1 + relationships: Optional[Relationships1] = None class Tracks(BaseModel): @@ -148,12 +148,22 @@ class Relationships(BaseModel): record_labels: RecordLabels = Field(..., alias='record-labels') +class ContentVersion(BaseModel): + MZ_INDEXER: Optional[int] = None + RTCI: Optional[int] = None + + +class Meta(BaseModel): + contentVersion: ContentVersion + + class Datum(BaseModel): id: Optional[str] = None type: Optional[str] = None href: Optional[str] = None attributes: Attributes relationships: Relationships + meta: Meta class AlbumMeta(BaseModel): diff --git a/src/models/song_data.py b/src/models/song_data.py index 272b844..f56611c 100644 --- a/src/models/song_data.py +++ b/src/models/song_data.py @@ -58,6 +58,7 @@ class Attributes(BaseModel): previews: List[Preview] artistName: Optional[str] = None extendedAssetUrls: ExtendedAssetUrls + contentRating: Optional[str] = None class Artwork1(BaseModel): diff --git a/src/utils.py b/src/utils.py index dc42f34..cc478ef 100644 --- a/src/utils.py +++ b/src/utils.py @@ -13,6 +13,8 @@ from src.exceptions import NotTimeSyncedLyricsException from src.models import PlaylistInfo from src.types import * +from copy import deepcopy + def check_url(url): pattern = regex.compile( @@ -82,6 +84,8 @@ def ttml_convent_to_lrc(ttml: str) -> str: lyric_time: str = lyric.get("begin") if not lyric_time: raise NotTimeSyncedLyricsException + if lyric_time.find('.') == -1: + lyric_time += '.000' match lyric_time.count(":"): case 0: split_time = lyric_time.split(".") @@ -146,17 +150,26 @@ def playlist_metadata_to_params(playlist: PlaylistInfo): return {"playlistName": playlist.data[0].attributes.name, "playlistCuratorName": playlist.data[0].attributes.curatorName} +def get_path_safe_dict(param: dict): + new_param = deepcopy(param) + for key, val in new_param.items(): + if isinstance(val, str): + new_param[key] = get_valid_filename(str(val)) + return new_param def get_song_name_and_dir_path(codec: str, config: Download, metadata, playlist: PlaylistInfo = None): if playlist: + safe_meta = get_path_safe_dict(metadata.model_dump()) + safe_pl_meta = get_path_safe_dict(playlist_metadata_to_params(playlist)) song_name = config.playlistSongNameFormat.format(codec=codec, playlistSongIndex=metadata.playlistIndex, - **metadata.model_dump()) + **safe_meta) dir_path = Path(config.playlistDirPathFormat.format(codec=codec, - **metadata.model_dump(), - **playlist_metadata_to_params(playlist))) + **safe_meta, + **safe_pl_meta)) else: - song_name = config.songNameFormat.format(codec=codec, **metadata.model_dump()) - dir_path = Path(config.dirPathFormat.format(codec=codec, **metadata.model_dump())) + safe_meta = get_path_safe_dict(metadata.model_dump()) + song_name = config.songNameFormat.format(codec=codec, **safe_meta) + dir_path = Path(config.dirPathFormat.format(codec=codec, **safe_meta)) if sys.platform == "win32": song_name = get_valid_filename(song_name) dir_path = Path(*[get_valid_filename(part) if ":\\" not in part else part for part in dir_path.parts])