feat: specify song metadata language
Some checks failed
/ Build Windows (push) Has been cancelled

This commit is contained in:
世界观察日志
2025-08-26 20:34:29 +08:00
parent 0491293bce
commit 83ec73e395
9 changed files with 2355 additions and 31 deletions

View File

@@ -17,6 +17,8 @@ dl https://music.apple.com/jp/album/nameless-name-single/1688539265
dl -c aac https://music.apple.com/jp/song/caribbean-blue/339592231
# Overwrite existing files
dl -f https://music.apple.com/jp/song/caribbean-blue/339592231
# Specify song metadata language
dl -l en-US https://music.apple.com/jp/album/nameless-name-single/1688539265
# Download specify artist's all albums
dl https://music.apple.com/jp/artist/%E3%83%88%E3%82%B2%E3%83%8A%E3%82%B7%E3%83%88%E3%82%B2%E3%82%A2%E3%83%AA/1688539273
# Download specify artist's all songs
@@ -38,7 +40,7 @@ dl https://music.apple.com/jp/playlist/bocchi-the-rock/pl.u-Ympg5s39LRqp
# Support Link
- Apple Music Song Share
Link (https://music.apple.com/jp/album/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E4%BD%95%E3%82%82%E3%81%8B%E3%82%82/1688539265?i=1688539274)
Link ( )
- Apple Music Album Share Link (https://music.apple.com/jp/album/nameless-name-single/1688539265)
- Apple Music Song Link (https://music.apple.com/jp/song/caribbean-blue/339592231)
- Apple Music Artist Link (https://music.apple.com/jp/artist/%E3%82%A8%E3%83%B3%E3%83%A4/160847)

2301
assets/storefronts.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,12 @@ url = "127.0.0.1:8080"
secure = false
[region]
# Used when the song region cannot be obtained, currently only used for m3u8 command and mitm command
# Recommend value: Your Apple Music's subscription region
defaultStorefront = "hk"
# The language of song metadata. Influence song title, author and other information.
# Simplified Chinese: zh-Hans-CN, Tradidional Chinese(Hong Kong): zh-Hant-HK, Tradidional Chinese(Taiwan): zh-Hant-TW
# English(British): en-GB, English(American): en-US, Japanese: ja, Korean: ko
language = "zh-Hant-HK"
# Pring warning when selected language does not exist on songs' region
languageNotExistWarning = true
[download]
# Send request to Apple Music through proxy. Only support http and https protocol
@@ -30,7 +29,7 @@ codecAlternative = true
codecPriority = ["alac", "ec3", "ac3", "aac"]
# Encapsulate Atmos(ec-3/ac-3) as M4A and write the song metadata
atmosConventToM4a = true
# Follow the Python Format format (https://docs.python.org/3/library/string.html#formatstrings)
# Follow the Python Format (https://docs.python.org/3/library/string.html#formatstrings)
# Write the audio information to the songNameFormat and playlistSongNameFormat
# Only support alac codec
# Available values: bit_depth, sample_rate, sample_rate_kHz, codec
@@ -73,10 +72,6 @@ saveLyrics = true
saveCover = true
coverFormat = "jpg"
coverSize = "5000x5000"
# 192000 96000 48000 44100
alacMax = 192000
# 2768 2448
atmosMax = 2768
# The command to be executed after the song is ripped successfully, passing in the filename parameter.
# Example: "\"C:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\" \"{filename}\""
# Pay attention to escaping issues

View File

@@ -42,6 +42,7 @@ class InteractiveShell:
choices=["alac", "ec3", "aac", "aac-binaural", "aac-downmix", "aac-legacy", "ac3"],
default="alac")
download_parser.add_argument("-f", "--force", default=False, action="store_true")
download_parser.add_argument("-l", "--language", default=it(Config).region.language, action="store")
download_parser.add_argument("--include-participate-songs", default=False, dest="include", action="store_true")
subparser.add_parser("status")
@@ -62,14 +63,14 @@ class InteractiveShell:
return
match cmds[0]:
case "download" | "dl":
await self.do_download(args.url, args.codec, args.force, args.include)
await self.do_download(args.url, args.codec, args.force, args.language, args.include)
case "status":
await self.show_status()
case "exit":
self.loop.stop()
sys.exit()
async def do_download(self, raw_url: str, codec: str, force_download: bool, include: bool = False):
async def do_download(self, raw_url: str, codec: str, force_download: bool, language: str, include: bool = False):
url = AppleMusicURL.parse_url(raw_url)
if not url:
real_url = await it(WebAPI).get_real_url(raw_url)
@@ -79,13 +80,14 @@ class InteractiveShell:
return
match url.type:
case URLType.Song:
safely_create_task(rip_song(url, codec, Flags(force_save=force_download)))
safely_create_task(rip_song(url, codec, Flags(force_save=force_download, language=language)))
case URLType.Album:
safely_create_task(rip_album(url, codec, Flags(force_save=force_download)))
safely_create_task(rip_album(url, codec, Flags(force_save=force_download, language=language)))
case URLType.Artist:
safely_create_task(rip_artist(url, codec, Flags(force_save=force_download, include_participate_in_works=include)))
safely_create_task(rip_artist(url, codec, Flags(force_save=force_download, language=language,
include_participate_in_works=include)))
case URLType.Playlist:
safely_create_task(rip_playlist(url, codec, Flags(force_save=force_download)))
safely_create_task(rip_playlist(url, codec, Flags(force_save=force_download, language=language)))
case _:
it(GlobalLogger).logger.error("Unsupported URLType")
return

View File

@@ -10,9 +10,10 @@ class Instance(BaseModel):
url: str
secure: bool
class Region(BaseModel):
language: str
defaultStorefront: str
languageNotExistWarning: bool
class Download(BaseModel):

View File

@@ -1,7 +1,12 @@
from dataclasses import dataclass
from creart import it
from src.config import Config
@dataclass
class Flags:
force_save: bool = False
include_participate_in_works: bool = False
language: str = it(Config).region.language

View File

@@ -71,6 +71,9 @@ class RipLogger:
self.logger.error(
f"Unable to download {self.item_type}. This {self.item_type} does not exist in all available storefronts")
def language_not_exist(self, region: str, current_language: str, default_language: str):
self.logger.warning(f"Selected language {current_language} does not exist in region {region.upper()}, falling back to {default_language}")
def already_exist(self):
self.logger.info(f"Song already exists")

View File

@@ -19,7 +19,7 @@ from src.task import Task, Status
from src.types import Codec, ParentDoneHandler
from src.url import Song, Album, URLType, Playlist
from src.utils import get_codec_from_codec_id, check_song_existence, check_song_exists, if_raw_atmos, \
check_album_existence, playlist_write_song_index, run_sync, safely_create_task
check_album_existence, playlist_write_song_index, run_sync, safely_create_task, language_exist, query_language
from src.legacy.mp4 import extract_media as legacy_extract_media
from src.legacy.mp4 import decrypt as legacy_decrypt
from src.legacy.decrypt import WidevineDecrypt
@@ -90,15 +90,20 @@ async def rip_song(url: Song, codec: str, flags: Flags = Flags(),
await task_lock.acquire()
# Set Metadata
raw_metadata = await it(WebAPI).get_song_info(task.adamId, url.storefront, it(Config).region.language)
raw_metadata = await it(WebAPI).get_song_info(task.adamId, url.storefront, flags.language)
album_data = await it(WebAPI).get_album_info(raw_metadata.relationships.albums.data[0].id, url.storefront,
it(Config).region.language)
flags.language)
task.metadata = SongMetadata.parse_from_song_data(raw_metadata)
task.metadata.parse_from_album_data(album_data)
task.update_logger()
task.logger.create()
# Check language
if it(Config).region.languageNotExistWarning and not language_exist(url.storefront, flags.language):
default_language, _ = query_language(url.storefront)
task.logger.language_not_exist(url.storefront, flags.language, default_language)
if not await check_song_existence(url.id, url.storefront):
task.logger.not_exist()
await task_done(task, Status.FAILED)
@@ -106,12 +111,8 @@ async def rip_song(url: Song, codec: str, flags: Flags = Flags(),
await task.metadata.get_cover(it(Config).download.coverFormat, it(Config).download.coverSize)
if raw_metadata.attributes.hasTimeSyncedLyrics:
try:
task.metadata.lyrics = await it(WrapperManager).lyrics(task.adamId, it(Config).region.language,
task.metadata.lyrics = await it(WrapperManager).lyrics(task.adamId, flags.language,
url.storefront)
except WrapperManagerException as e:
if e.msg == "no available lyrics":
task.logger.lyrics_not_exist()
if playlist:
task.metadata.set_playlist_index(playlist.songIdIndexMapping.get(url.id))
@@ -202,7 +203,7 @@ async def rip_song_legacy(task: Task):
async def rip_album(url: Album, codec: str, flags: Flags = Flags(), parent_done: ParentDoneHandler = None):
album_info = await it(WebAPI).get_album_info(url.id, url.storefront, it(Config).region.language)
album_info = await it(WebAPI).get_album_info(url.id, url.storefront, flags.language)
logger = RipLogger(url.type, url.id)
logger.set_fullname(album_info.data[0].attributes.artistName, album_info.data[0].attributes.name)
@@ -224,7 +225,7 @@ async def rip_album(url: Album, codec: str, flags: Flags = Flags(), parent_done:
async def rip_artist(url: Album, codec: str, flags: Flags = Flags()):
artist_info = await it(WebAPI).get_artist_info(url.id, url.storefront, it(Config).region.language)
artist_info = await it(WebAPI).get_artist_info(url.id, url.storefront, flags.language)
logger = RipLogger(url.type, url.id)
logger.set_fullname(artist_info.data[0].attributes.name)
@@ -234,19 +235,19 @@ async def rip_artist(url: Album, codec: str, flags: Flags = Flags()):
logger.done()
if flags.include_participate_in_works:
songs = await it(WebAPI).get_songs_from_artist(url.id, url.storefront, it(Config).region.language)
songs = await it(WebAPI).get_songs_from_artist(url.id, url.storefront, flags.language)
done_handler = ParentDoneHandler(len(songs), on_children_done)
for song_url in songs:
safely_create_task(rip_song(Song.parse_url(song_url), codec, flags, done_handler))
else:
albums = await it(WebAPI).get_albums_from_artist(url.id, url.storefront, it(Config).region.language)
albums = await it(WebAPI).get_albums_from_artist(url.id, url.storefront, flags.language)
done_handler = ParentDoneHandler(len(albums), on_children_done)
for album_url in albums:
safely_create_task(rip_album(Album.parse_url(album_url), codec, flags, done_handler))
async def rip_playlist(url: Playlist, codec: str, flags: Flags = Flags()):
playlist_info = await it(WebAPI).get_playlist_info_and_tracks(url.id, url.storefront, it(Config).region.language)
playlist_info = await it(WebAPI).get_playlist_info_and_tracks(url.id, url.storefront, flags.language)
playlist_info = playlist_write_song_index(playlist_info)
logger = RipLogger(url.type, url.id)
logger.set_fullname(playlist_info.data[0].attributes.curatorName, playlist_info.data[0].attributes.name)

View File

@@ -1,5 +1,6 @@
import asyncio
import concurrent.futures
import json
import subprocess
import time
from asyncio import AbstractEventLoop
@@ -260,7 +261,6 @@ def safely_create_task(coro):
task = it(AbstractEventLoop).create_task(coro)
background_tasks.add(task)
def done_callback(*args):
background_tasks.remove(task)
if task.exception():
@@ -283,3 +283,17 @@ def count_total_track_and_disc(tracks: Tracks):
def get_tasks_num():
return len(background_tasks)
def query_language(region: str):
with open("assets/storefronts.json", "r") as f:
storefronts = json.load(f)
for storefront in storefronts["data"]:
if storefront["id"].upper() == region.upper():
return storefront["attributes"]["defaultLanguageTag"], storefront["attributes"]["supportedLanguageTags"]
return None
def language_exist(region: str, language: str):
_, languages = query_language(region)
return language in languages