mirror of
https://github.com/WorldObservationLog/AppleMusicDecrypt.git
synced 2026-01-15 14:22:54 -03:00
This commit is contained in:
@@ -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
2301
assets/storefronts.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
14
src/cmd.py
14
src/cmd.py
@@ -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
|
||||
|
||||
@@ -10,9 +10,10 @@ class Instance(BaseModel):
|
||||
url: str
|
||||
secure: bool
|
||||
|
||||
|
||||
class Region(BaseModel):
|
||||
language: str
|
||||
defaultStorefront: str
|
||||
languageNotExistWarning: bool
|
||||
|
||||
|
||||
class Download(BaseModel):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
29
src/rip.py
29
src/rip.py
@@ -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,
|
||||
url.storefront)
|
||||
except WrapperManagerException as e:
|
||||
if e.msg == "no available lyrics":
|
||||
task.logger.lyrics_not_exist()
|
||||
task.metadata.lyrics = await it(WrapperManager).lyrics(task.adamId, flags.language,
|
||||
url.storefront)
|
||||
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)
|
||||
|
||||
16
src/utils.py
16
src/utils.py
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user