mirror of
https://github.com/WorldObservationLog/AppleMusicDecrypt.git
synced 2026-01-15 22:32:54 -03:00
218 lines
9.2 KiB
Python
218 lines
9.2 KiB
Python
import asyncio
|
|
import subprocess
|
|
from asyncio import AbstractEventLoop
|
|
from typing import Dict
|
|
|
|
from creart import it
|
|
|
|
from src.api import WebAPI
|
|
from src.config import Config
|
|
from src.flags import Flags
|
|
from src.grpc.manager import WrapperManager
|
|
from src.logger import RipLogger
|
|
from src.measurer import SpeedMeasurer
|
|
from src.metadata import SongMetadata
|
|
from src.models import PlaylistInfo
|
|
from src.mp4 import extract_media, extract_song, encapsulate, write_metadata, fix_encapsulate, fix_esds_box, \
|
|
check_song_integrity
|
|
from src.save import save
|
|
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
|
|
|
|
# START -> getMetadata -> getLyrics -> getM3U8 -> downloadSong -> decrypt -> encapsulate -> save -> END
|
|
|
|
adam_id_task_mapping: Dict[str, Task] = {}
|
|
|
|
|
|
async def task_done(task: Task, status: Status):
|
|
task.update_status(status)
|
|
if task.parentDone:
|
|
await task.parentDone.try_done()
|
|
del adam_id_task_mapping[task.adamId]
|
|
|
|
|
|
async def on_decrypt_success(adam_id: str, key: str, sample: bytes, sample_index: int):
|
|
it(SpeedMeasurer).record_decrypt(len(sample))
|
|
it(AbstractEventLoop).create_task(recv_decrypted_sample(adam_id, sample_index, sample))
|
|
|
|
|
|
async def on_decrypt_failed(adam_id: str, key: str, sample: bytes, sample_index: int):
|
|
await it(WrapperManager).decrypt(adam_id, key, sample, sample_index)
|
|
|
|
|
|
async def recv_decrypted_sample(adam_id: str, sample_index: int, sample: bytes):
|
|
task = adam_id_task_mapping[adam_id]
|
|
task.decryptedSamples[sample_index] = sample
|
|
task.decryptedCount += 1
|
|
if task.decryptedCount == len(task.decryptedSamples):
|
|
it(AbstractEventLoop).create_task(decrypt_done(adam_id))
|
|
|
|
|
|
async def decrypt_done(adam_id: str):
|
|
task = adam_id_task_mapping[adam_id]
|
|
codec = get_codec_from_codec_id(task.m3u8Info.codec_id)
|
|
|
|
song = await run_sync(encapsulate, task.info, bytes().join(task.decryptedSamples),
|
|
it(Config).download.atmosConventToM4a)
|
|
if not if_raw_atmos(codec, it(Config).download.atmosConventToM4a):
|
|
song = await run_sync(write_metadata, song, task.metadata, it(Config).metadata.embedMetadata,
|
|
it(Config).download.coverFormat, task.info.params)
|
|
if codec != Codec.EC3 or codec != Codec.EC3:
|
|
song = await run_sync(fix_encapsulate, song)
|
|
if codec == Codec.AAC or codec == Codec.AAC_DOWNMIX or codec == Codec.AAC_BINAURAL:
|
|
song = await run_sync(fix_esds_box, task.info.raw, song)
|
|
|
|
song = await run_sync(write_metadata, song, task.metadata, it(Config).metadata.embedMetadata,
|
|
it(Config).download.coverFormat, task.info.params)
|
|
|
|
if not await run_sync(check_song_integrity, song):
|
|
task.logger.failed_integrity()
|
|
|
|
filename = await run_sync(save, song, codec, task.metadata, task.playlist)
|
|
task.logger.saved()
|
|
|
|
await task_done(task, Status.DONE)
|
|
|
|
if it(Config).download.afterDownloaded:
|
|
command = it(Config).download.afterDownloaded.format(filename=filename)
|
|
subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
async def rip_song(url: Song, codec: str, flags: Flags = Flags(),
|
|
parent_done: ParentDoneHandler = None, playlist: PlaylistInfo = None):
|
|
task = Task(adam_id=url.id, parent_done=parent_done, playlist=playlist)
|
|
adam_id_task_mapping[url.id] = task
|
|
task.init_logger()
|
|
|
|
# Set Metadata
|
|
raw_metadata = await it(WebAPI).get_song_info(task.adamId, url.storefront, it(Config).region.language)
|
|
task.metadata = SongMetadata.parse_from_song_data(raw_metadata)
|
|
|
|
task.update_logger()
|
|
task.logger.create()
|
|
|
|
if not await check_song_existence(url.id, url.storefront):
|
|
task.logger.not_exist()
|
|
await task_done(task, Status.FAILED)
|
|
return
|
|
|
|
await task.metadata.get_cover(it(Config).download.coverFormat, it(Config).download.coverSize)
|
|
if raw_metadata.attributes.hasTimeSyncedLyrics:
|
|
task.metadata.lyrics = await it(WrapperManager).lyrics(task.adamId, it(Config).region.language,
|
|
it(Config).region.defaultStorefront)
|
|
if playlist:
|
|
task.metadata.set_playlist_index(playlist.songIdIndexMapping.get(url.id))
|
|
|
|
# Check existence
|
|
if not flags.force_save and check_song_exists(task.metadata, codec, playlist):
|
|
task.logger.already_exist()
|
|
await task_done(task, Status.DONE)
|
|
return
|
|
|
|
# Get M3U8
|
|
if not raw_metadata.attributes.extendedAssetUrls:
|
|
task.logger.audio_not_exist()
|
|
await task_done(task, Status.FAILED)
|
|
return
|
|
if codec == Codec.ALAC and raw_metadata.attributes.extendedAssetUrls.enhancedHls:
|
|
m3u8_url = await it(WrapperManager).m3u8(task.adamId)
|
|
else:
|
|
m3u8_url = raw_metadata.attributes.extendedAssetUrls.enhancedHls
|
|
if not m3u8_url and not raw_metadata.attributes.extendedAssetUrls.enhancedHls:
|
|
task.logger.lossless_audio_not_exist()
|
|
await task_done(task, Status.FAILED)
|
|
return
|
|
|
|
task.m3u8Info = await extract_media(m3u8_url, codec, task.metadata)
|
|
task.logger.selected_codec(task.m3u8Info.codec_id)
|
|
if all([bool(task.m3u8Info.bit_depth), bool(task.m3u8Info.sample_rate)]):
|
|
task.metadata.set_bit_depth_and_sample_rate(task.m3u8Info.bit_depth, task.m3u8Info.sample_rate)
|
|
# Check existence again
|
|
if not flags.force_save and check_song_exists(task.metadata, codec, playlist):
|
|
task.logger.already_exist()
|
|
await task_done(task, Status.DONE)
|
|
return
|
|
|
|
# Download
|
|
task.logger.downloading()
|
|
task.update_status(Status.DOWNLOADING)
|
|
raw_song = await it(WebAPI).download_song(task.m3u8Info.uri)
|
|
|
|
# Decrypt
|
|
task.logger.decrypting()
|
|
task.update_status(Status.DECRYPTING)
|
|
codec = get_codec_from_codec_id(task.m3u8Info.codec_id)
|
|
task.info = await run_sync(extract_song, raw_song, codec)
|
|
task.init_decrypted_samples()
|
|
for sampleIndex, sample in enumerate(task.info.samples):
|
|
await it(WrapperManager).decrypt(task.adamId, task.m3u8Info.keys[sample.descIndex], sample.data, sampleIndex)
|
|
|
|
|
|
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)
|
|
logger = RipLogger(url.type, url.id)
|
|
logger.set_fullname(album_info.data[0].attributes.artistName, album_info.data[0].attributes.name)
|
|
|
|
logger.create()
|
|
if not await check_album_existence(url.id, url.storefront):
|
|
logger.not_exist()
|
|
return
|
|
|
|
async def on_children_done():
|
|
logger.done()
|
|
if parent_done:
|
|
await parent_done.try_done()
|
|
|
|
done_handler = ParentDoneHandler(len(album_info.data[0].relationships.tracks.data), on_children_done)
|
|
|
|
async with asyncio.TaskGroup() as tg:
|
|
for track in album_info.data[0].relationships.tracks.data:
|
|
song = Song(id=track.id, storefront=url.storefront, url="", type=URLType.Song)
|
|
tg.create_task(rip_song(song, codec, flags, done_handler))
|
|
|
|
|
|
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)
|
|
logger = RipLogger(url.type, url.id)
|
|
logger.set_fullname(artist_info.data[0].attributes.name)
|
|
|
|
logger.create()
|
|
|
|
async def on_children_done():
|
|
logger.done()
|
|
|
|
async with asyncio.TaskGroup() as tg:
|
|
if flags.include_participate_in_works:
|
|
songs = await it(WebAPI).get_songs_from_artist(url.id, url.storefront, it(Config).region.language)
|
|
done_handler = ParentDoneHandler(len(songs), on_children_done)
|
|
for song_url in songs:
|
|
tg.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)
|
|
done_handler = ParentDoneHandler(len(albums), on_children_done)
|
|
for album_url in albums:
|
|
tg.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 = 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)
|
|
|
|
logger.create()
|
|
|
|
async def on_children_done():
|
|
logger.done()
|
|
|
|
done_handler = ParentDoneHandler(len(playlist_info.data[0].relationships.tracks.data), on_children_done)
|
|
|
|
async with asyncio.TaskGroup() as tg:
|
|
for track in playlist_info.data[0].relationships.tracks.data:
|
|
song = Song(id=track.id, storefront=url.storefront, url="", type=URLType.Song)
|
|
tg.create_task(
|
|
rip_song(song, codec, flags, done_handler, playlist=playlist_info))
|