mirror of
https://github.com/WorldObservationLog/AppleMusicDecrypt.git
synced 2026-01-15 14:22:54 -03:00
feat: add status parameter to process
This commit is contained in:
12
src/api.py
12
src/api.py
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from io import BytesIO
|
||||||
from ssl import SSLError
|
from ssl import SSLError
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_
|
|||||||
|
|
||||||
from src.models import *
|
from src.models import *
|
||||||
from src.models.song_data import Datum
|
from src.models.song_data import Datum
|
||||||
|
from src.status import BaseStatus, StatusCode
|
||||||
|
|
||||||
client: httpx.AsyncClient
|
client: httpx.AsyncClient
|
||||||
download_lock: asyncio.Semaphore
|
download_lock: asyncio.Semaphore
|
||||||
@@ -78,9 +80,15 @@ async def get_token():
|
|||||||
@retry(retry=retry_if_exception_type((httpx.HTTPError, SSLError, FileNotFoundError)),
|
@retry(retry=retry_if_exception_type((httpx.HTTPError, SSLError, FileNotFoundError)),
|
||||||
wait=wait_random_exponential(multiplier=1, max=60),
|
wait=wait_random_exponential(multiplier=1, max=60),
|
||||||
stop=stop_after_attempt(retry_times), before_sleep=before_sleep_log(logger, logging.WARN))
|
stop=stop_after_attempt(retry_times), before_sleep=before_sleep_log(logger, logging.WARN))
|
||||||
async def download_song(url: str) -> bytes:
|
async def download_song(url: str, status: BaseStatus) -> bytes:
|
||||||
async with download_lock:
|
async with download_lock:
|
||||||
return (await client.get(url)).content
|
status.set_status(StatusCode.Downloading)
|
||||||
|
result = BytesIO()
|
||||||
|
async with client.stream("GET", url) as resp:
|
||||||
|
async for chunk in resp.aiter_bytes():
|
||||||
|
result.write(chunk)
|
||||||
|
status.set_progress("download", resp.num_bytes_downloaded, int(resp.headers["Content-Length"]))
|
||||||
|
return result.getvalue()
|
||||||
|
|
||||||
|
|
||||||
@alru_cache
|
@alru_cache
|
||||||
|
|||||||
24
src/cmd.py
24
src/cmd.py
@@ -3,6 +3,7 @@ import asyncio
|
|||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
from asyncio import Task
|
from asyncio import Task
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from prompt_toolkit import PromptSession, print_formatted_text, ANSI
|
from prompt_toolkit import PromptSession, print_formatted_text, ANSI
|
||||||
@@ -12,6 +13,7 @@ from src.adb import Device
|
|||||||
from src.api import get_token, init_client_and_lock, upload_m3u8_to_api, get_song_info, get_real_url
|
from src.api import get_token, init_client_and_lock, upload_m3u8_to_api, get_song_info, get_real_url
|
||||||
from src.config import Config
|
from src.config import Config
|
||||||
from src.rip import rip_song, rip_album, rip_artist, rip_playlist
|
from src.rip import rip_song, rip_album, rip_artist, rip_playlist
|
||||||
|
from src.status import LogStatus, BaseStatus
|
||||||
from src.types import GlobalAuthParams
|
from src.types import GlobalAuthParams
|
||||||
from src.url import AppleMusicURL, URLType, Song
|
from src.url import AppleMusicURL, URLType, Song
|
||||||
from src.utils import get_song_id_from_m3u8
|
from src.utils import get_song_id_from_m3u8
|
||||||
@@ -105,17 +107,25 @@ class NewInteractiveShell:
|
|||||||
self.anonymous_access_token)
|
self.anonymous_access_token)
|
||||||
match url.type:
|
match url.type:
|
||||||
case URLType.Song:
|
case URLType.Song:
|
||||||
|
status = LogStatus(status_type=URLType.Song)
|
||||||
task = self.loop.create_task(
|
task = self.loop.create_task(
|
||||||
rip_song(url, global_auth_param, codec, self.config, available_device, force_download))
|
rip_song(url, global_auth_param, codec, self.config, available_device, status,
|
||||||
|
force_save=force_download))
|
||||||
case URLType.Album:
|
case URLType.Album:
|
||||||
task = self.loop.create_task(rip_album(url, global_auth_param, codec, self.config, available_device,
|
status = LogStatus(status_type=URLType.Album)
|
||||||
force_download))
|
task = self.loop.create_task(
|
||||||
|
rip_album(url, global_auth_param, codec, self.config, available_device, status,
|
||||||
|
force_save=force_download))
|
||||||
case URLType.Artist:
|
case URLType.Artist:
|
||||||
task = self.loop.create_task(rip_artist(url, global_auth_param, codec, self.config, available_device,
|
status = LogStatus(status_type=URLType.Artist)
|
||||||
force_download, include))
|
task = self.loop.create_task(
|
||||||
|
rip_artist(url, global_auth_param, codec, self.config, available_device, status,
|
||||||
|
force_save=force_download, include_participate_in_works=include))
|
||||||
case URLType.Playlist:
|
case URLType.Playlist:
|
||||||
task = self.loop.create_task(rip_playlist(url, global_auth_param, codec, self.config, available_device,
|
status = LogStatus(status_type=URLType.Playlist)
|
||||||
force_download))
|
task = self.loop.create_task(
|
||||||
|
rip_playlist(url, global_auth_param, codec, self.config, available_device, status,
|
||||||
|
force_save=force_download))
|
||||||
case _:
|
case _:
|
||||||
logger.error("Unsupported URLType")
|
logger.error("Unsupported URLType")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ retry_count = {}
|
|||||||
@retry(retry=retry_if_exception_type(RetryableDecryptException), stop=stop_after_attempt(3),
|
@retry(retry=retry_if_exception_type(RetryableDecryptException), stop=stop_after_attempt(3),
|
||||||
before_sleep=before_sleep_log(logger, logging.WARN))
|
before_sleep=before_sleep_log(logger, logging.WARN))
|
||||||
@timeit
|
@timeit
|
||||||
async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Device | HyperDecryptDevice) -> bytes:
|
async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Device | HyperDecryptDevice, status: BaseStatus) -> bytes:
|
||||||
async with device.decryptLock:
|
async with device.decryptLock:
|
||||||
|
status.set_status(StatusCode.Decrypting)
|
||||||
if isinstance(device, HyperDecryptDevice):
|
if isinstance(device, HyperDecryptDevice):
|
||||||
logger.info(f"Using hyperDecryptDevice {device.serial} to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}")
|
logger.info(f"Using hyperDecryptDevice {device.serial} to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}")
|
||||||
else:
|
else:
|
||||||
@@ -31,7 +32,9 @@ async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Devi
|
|||||||
raise RetryableDecryptException
|
raise RetryableDecryptException
|
||||||
decrypted = []
|
decrypted = []
|
||||||
last_index = 255
|
last_index = 255
|
||||||
|
now = 0
|
||||||
for sample in info.samples:
|
for sample in info.samples:
|
||||||
|
status.set_progress("decrypt", now, len(info.samples))
|
||||||
if last_index != sample.descIndex:
|
if last_index != sample.descIndex:
|
||||||
if len(decrypted) != 0:
|
if len(decrypted) != 0:
|
||||||
writer.write(bytes([0, 0, 0, 0]))
|
writer.write(bytes([0, 0, 0, 0]))
|
||||||
|
|||||||
114
src/rip.py
114
src/rip.py
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ from src.metadata import SongMetadata
|
|||||||
from src.models import PlaylistInfo
|
from src.models import PlaylistInfo
|
||||||
from src.mp4 import extract_media, extract_song, encapsulate, write_metadata, fix_encapsulate, fix_esds_box
|
from src.mp4 import extract_media, extract_song, encapsulate, write_metadata, fix_encapsulate, fix_esds_box
|
||||||
from src.save import save
|
from src.save import save
|
||||||
|
from src.status import BaseStatus, StatusCode, ErrorCode, WarningCode
|
||||||
from src.types import GlobalAuthParams, Codec
|
from src.types import GlobalAuthParams, Codec
|
||||||
from src.url import Song, Album, URLType, Artist, Playlist
|
from src.url import Song, Album, URLType, Artist, Playlist
|
||||||
from src.utils import check_song_exists, if_raw_atmos, playlist_write_song_index, get_codec_from_codec_id, timeit
|
from src.utils import check_song_exists, if_raw_atmos, playlist_write_song_index, get_codec_from_codec_id, timeit
|
||||||
@@ -23,51 +25,49 @@ task_lock = asyncio.Semaphore(16)
|
|||||||
|
|
||||||
@logger.catch
|
@logger.catch
|
||||||
@timeit
|
@timeit
|
||||||
async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
|
async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device, status: BaseStatus,
|
||||||
force_save: bool = False, specified_m3u8: str = "", playlist: PlaylistInfo = None):
|
force_save: bool = False, specified_m3u8: str = "", playlist: PlaylistInfo = None, return_result: bool = False) -> Optional[tuple[bytes, SongMetadata, str]]:
|
||||||
async with task_lock:
|
async with task_lock:
|
||||||
logger.debug(f"Task of song id {song.id} was created")
|
status.set_param(song_id=song.id)
|
||||||
|
status.set_status(StatusCode.Processing)
|
||||||
token = auth_params.anonymousAccessToken
|
token = auth_params.anonymousAccessToken
|
||||||
song_data = await get_song_info(song.id, token, song.storefront, config.region.language)
|
song_data = await get_song_info(song.id, token, song.storefront, config.region.language)
|
||||||
song_metadata = SongMetadata.parse_from_song_data(song_data)
|
song_metadata = SongMetadata.parse_from_song_data(song_data)
|
||||||
|
status.set_param(artist=song_metadata.artist, title=song_metadata.title,
|
||||||
|
song_storefront=song.storefront, storefront=auth_params.storefront)
|
||||||
if playlist:
|
if playlist:
|
||||||
song_metadata.set_playlist_index(playlist.songIdIndexMapping.get(song.id))
|
song_metadata.set_playlist_index(playlist.songIdIndexMapping.get(song.id))
|
||||||
logger.info(f"Ripping song: {song_metadata.artist} - {song_metadata.title}")
|
status.set_status(StatusCode.Parsing)
|
||||||
if not await exist_on_storefront_by_song_id(song.id, song.storefront, auth_params.storefront,
|
if not await exist_on_storefront_by_song_id(song.id, song.storefront, auth_params.storefront,
|
||||||
auth_params.anonymousAccessToken, config.region.language):
|
auth_params.anonymousAccessToken, config.region.language):
|
||||||
logger.error(
|
status.set_status(ErrorCode.NotExistInStorefront)
|
||||||
f"Unable to download song {song_metadata.artist} - {song_metadata.title}. "
|
|
||||||
f"This song does not exist in storefront {auth_params.storefront.upper()} "
|
|
||||||
f"and no device is available to decrypt it")
|
|
||||||
return
|
return
|
||||||
if not force_save and check_song_exists(song_metadata, config.download, codec, playlist):
|
if not force_save and check_song_exists(song_metadata, config.download, codec, playlist) and not return_result:
|
||||||
logger.info(f"Song: {song_metadata.artist} - {song_metadata.title} already exists")
|
status.set_status(StatusCode.AlreadyExist)
|
||||||
return
|
return
|
||||||
await song_metadata.get_cover(config.download.coverFormat, config.download.coverSize)
|
await song_metadata.get_cover(config.download.coverFormat, config.download.coverSize)
|
||||||
if song_data.attributes.hasTimeSyncedLyrics:
|
if song_data.attributes.hasTimeSyncedLyrics:
|
||||||
if song.storefront.upper() != auth_params.storefront.upper():
|
if song.storefront.upper() != auth_params.storefront.upper():
|
||||||
logger.warning(f"No account is available for getting lyrics of storefront {song.storefront.upper()}. "
|
status.set_warning(WarningCode.NoAvailableAccountForLyrics)
|
||||||
f"Use storefront {auth_params.storefront.upper()} to get lyrics")
|
|
||||||
lyrics = await get_song_lyrics(song.id, auth_params.storefront, auth_params.accountAccessToken,
|
lyrics = await get_song_lyrics(song.id, auth_params.storefront, auth_params.accountAccessToken,
|
||||||
auth_params.dsid, auth_params.accountToken, config.region.language)
|
auth_params.dsid, auth_params.accountToken, config.region.language)
|
||||||
if lyrics:
|
if lyrics:
|
||||||
song_metadata.lyrics = lyrics
|
song_metadata.lyrics = lyrics
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unable to get lyrics of song: {song_metadata.artist} - {song_metadata.title}")
|
status.set_warning(WarningCode.UnableGetLyrics)
|
||||||
if config.m3u8Api.enable and codec == Codec.ALAC and not specified_m3u8:
|
if config.m3u8Api.enable and codec == Codec.ALAC and not specified_m3u8:
|
||||||
m3u8_url = await get_m3u8_from_api(config.m3u8Api.endpoint, song.id, config.m3u8Api.enable)
|
m3u8_url = await get_m3u8_from_api(config.m3u8Api.endpoint, song.id, config.m3u8Api.enable)
|
||||||
if m3u8_url:
|
if m3u8_url:
|
||||||
specified_m3u8 = m3u8_url
|
specified_m3u8 = m3u8_url
|
||||||
logger.info(f"Use m3u8 from API for song: {song_metadata.artist} - {song_metadata.title}")
|
logger.info(f"Use m3u8 from API for song: {song_metadata.artist} - {song_metadata.title}")
|
||||||
elif not m3u8_url and config.m3u8Api.force:
|
elif not m3u8_url and config.m3u8Api.force:
|
||||||
logger.error(f"Failed to get m3u8 from API for song: {song_metadata.artist} - {song_metadata.title}")
|
status.set_error(ErrorCode.ForceModeM3U8NotExist)
|
||||||
return
|
return
|
||||||
if not song_data.attributes.extendedAssetUrls:
|
if not song_data.attributes.extendedAssetUrls:
|
||||||
logger.error(
|
status.set_error(ErrorCode.AudioNotExist)
|
||||||
f"Failed to download song: {song_metadata.artist} - {song_metadata.title}. Audio does not exist")
|
|
||||||
return
|
return
|
||||||
if not specified_m3u8 and not song_data.attributes.extendedAssetUrls.enhancedHls:
|
if not specified_m3u8 and not song_data.attributes.extendedAssetUrls.enhancedHls:
|
||||||
logger.error(f"Failed to download song: {song_metadata.artist} - {song_metadata.title}. Lossless audio does not exist")
|
status.set_error(ErrorCode.LosslessAudioNotExist)
|
||||||
return
|
return
|
||||||
if not specified_m3u8:
|
if not specified_m3u8:
|
||||||
device_m3u8 = await device.get_m3u8(song.id)
|
device_m3u8 = await device.get_m3u8(song.id)
|
||||||
@@ -83,89 +83,103 @@ async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config
|
|||||||
song_metadata,
|
song_metadata,
|
||||||
config.download.codecPriority,
|
config.download.codecPriority,
|
||||||
config.download.codecAlternative)
|
config.download.codecAlternative)
|
||||||
logger.info(f"Downloading song: {song_metadata.artist} - {song_metadata.title}")
|
status.set_param(codec=codec_id)
|
||||||
codec = get_codec_from_codec_id(codec_id)
|
codec = get_codec_from_codec_id(codec_id)
|
||||||
raw_song = await download_song(song_uri)
|
raw_song = await download_song(song_uri, status)
|
||||||
song_info = await extract_song(raw_song, codec)
|
song_info = await extract_song(raw_song, codec)
|
||||||
if device.hyperDecryptDevices:
|
if device.hyperDecryptDevices:
|
||||||
if all([hyper_device.decryptLock.locked() for hyper_device in device.hyperDecryptDevices]):
|
if all([hyper_device.decryptLock.locked() for hyper_device in device.hyperDecryptDevices]):
|
||||||
decrypted_song = await decrypt(song_info, keys, song_data, random.choice(device.hyperDecryptDevices))
|
decrypted_song = await decrypt(song_info, keys, song_data, random.choice(device.hyperDecryptDevices), status)
|
||||||
else:
|
else:
|
||||||
for hyperDecryptDevice in device.hyperDecryptDevices:
|
for hyperDecryptDevice in device.hyperDecryptDevices:
|
||||||
if not hyperDecryptDevice.decryptLock.locked():
|
if not hyperDecryptDevice.decryptLock.locked():
|
||||||
decrypted_song = await decrypt(song_info, keys, song_data, hyperDecryptDevice)
|
decrypted_song = await decrypt(song_info, keys, song_data, hyperDecryptDevice, status)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
decrypted_song = await decrypt(song_info, keys, song_data, device)
|
decrypted_song = await decrypt(song_info, keys, song_data, device, status)
|
||||||
|
status.set_status(StatusCode.Saving)
|
||||||
song = await encapsulate(song_info, decrypted_song, config.download.atmosConventToM4a)
|
song = await encapsulate(song_info, decrypted_song, config.download.atmosConventToM4a)
|
||||||
if not if_raw_atmos(codec, config.download.atmosConventToM4a):
|
if not if_raw_atmos(codec, config.download.atmosConventToM4a):
|
||||||
metadata_song = await write_metadata(song, song_metadata, config.metadata.embedMetadata, config.download.coverFormat)
|
metadata_song = await write_metadata(song, song_metadata, config.metadata.embedMetadata,
|
||||||
|
config.download.coverFormat)
|
||||||
song = await fix_encapsulate(metadata_song)
|
song = await fix_encapsulate(metadata_song)
|
||||||
if codec == Codec.AAC or codec == Codec.AAC_DOWNMIX or codec == Codec.AAC_BINAURAL:
|
if codec == Codec.AAC or codec == Codec.AAC_DOWNMIX or codec == Codec.AAC_BINAURAL:
|
||||||
song = await fix_esds_box(song_info.raw, song)
|
song = await fix_esds_box(song_info.raw, song)
|
||||||
filename = await save(song, codec, song_metadata, config.download, playlist)
|
if return_result:
|
||||||
logger.info(f"Song {song_metadata.artist} - {song_metadata.title} saved!")
|
status.set_status(StatusCode.Done)
|
||||||
if config.download.afterDownloaded:
|
return song, song_metadata, codec
|
||||||
command = config.download.afterDownloaded.format(filename=filename)
|
else:
|
||||||
logger.info(f"Executing command: {command}")
|
filename = await save(song, codec, song_metadata, config.download, playlist)
|
||||||
subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
status.set_status(StatusCode.Done)
|
||||||
|
if config.download.afterDownloaded:
|
||||||
|
command = config.download.afterDownloaded.format(filename=filename)
|
||||||
|
logger.info(f"Executing command: {command}")
|
||||||
|
subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
@logger.catch
|
@logger.catch
|
||||||
@timeit
|
@timeit
|
||||||
async def rip_album(album: Album, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
|
async def rip_album(album: Album, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device, status: BaseStatus,
|
||||||
force_save: bool = False):
|
force_save: bool = False):
|
||||||
album_info = await get_album_info(album.id, auth_params.anonymousAccessToken, album.storefront,
|
album_info = await get_album_info(album.id, auth_params.anonymousAccessToken, album.storefront,
|
||||||
config.region.language)
|
config.region.language)
|
||||||
logger.info(f"Ripping Album: {album_info.data[0].attributes.artistName} - {album_info.data[0].attributes.name}")
|
status.set_param(artist=album_info.data[0].attributes.artistName, title=album_info.data[0].attributes.name,
|
||||||
if not await exist_on_storefront_by_album_id(album.id, album.storefront, auth_params.storefront, auth_params.anonymousAccessToken, config.region.language):
|
storefront=auth_params.storefront)
|
||||||
logger.error(f"Unable to download album {album_info.data[0].attributes.artistName} - {album_info.data[0].attributes.name}. "
|
status.set_status(StatusCode.Processing)
|
||||||
f"This album does not exist in storefront {auth_params.storefront.upper()} "
|
if not await exist_on_storefront_by_album_id(album.id, album.storefront, auth_params.storefront,
|
||||||
f"and no device is available to decrypt it")
|
auth_params.anonymousAccessToken, config.region.language):
|
||||||
|
status.set_error(ErrorCode.NotExistInStorefront)
|
||||||
return
|
return
|
||||||
async with asyncio.TaskGroup() as tg:
|
async with asyncio.TaskGroup() as tg:
|
||||||
for track in album_info.data[0].relationships.tracks.data:
|
for track in album_info.data[0].relationships.tracks.data:
|
||||||
|
song_status = status.new(URLType.Song)
|
||||||
|
status.children.append(song_status)
|
||||||
song = Song(id=track.id, storefront=album.storefront, url="", type=URLType.Song)
|
song = Song(id=track.id, storefront=album.storefront, url="", type=URLType.Song)
|
||||||
tg.create_task(rip_song(song, auth_params, codec, config, device, force_save=force_save))
|
tg.create_task(rip_song(song, auth_params, codec, config, device, song_status, force_save=force_save))
|
||||||
logger.info(
|
status.set_status(StatusCode.Done)
|
||||||
f"Album: {album_info.data[0].attributes.artistName} - {album_info.data[0].attributes.name} finished ripping")
|
|
||||||
|
|
||||||
|
|
||||||
@logger.catch
|
@logger.catch
|
||||||
@timeit
|
@timeit
|
||||||
async def rip_playlist(playlist: Playlist, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
|
async def rip_playlist(playlist: Playlist, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device, status: BaseStatus,
|
||||||
force_save: bool = False):
|
force_save: bool = False):
|
||||||
playlist_info = await get_playlist_info_and_tracks(playlist.id, auth_params.anonymousAccessToken,
|
playlist_info = await get_playlist_info_and_tracks(playlist.id, auth_params.anonymousAccessToken,
|
||||||
playlist.storefront,
|
playlist.storefront,
|
||||||
config.region.language)
|
config.region.language)
|
||||||
playlist_info = playlist_write_song_index(playlist_info)
|
playlist_info = playlist_write_song_index(playlist_info)
|
||||||
logger.info(
|
status.set_param(artist=playlist_info.data[0].attributes.curatorName, title=playlist_info.data[0].attributes.name)
|
||||||
f"Ripping Playlist: {playlist_info.data[0].attributes.curatorName} - {playlist_info.data[0].attributes.name}")
|
status.set_status(StatusCode.Processing)
|
||||||
async with asyncio.TaskGroup() as tg:
|
async with asyncio.TaskGroup() as tg:
|
||||||
for track in playlist_info.data[0].relationships.tracks.data:
|
for track in playlist_info.data[0].relationships.tracks.data:
|
||||||
|
song_status = status.new(URLType.Song)
|
||||||
|
status.children.append(song_status)
|
||||||
song = Song(id=track.id, storefront=playlist.storefront, url="", type=URLType.Song)
|
song = Song(id=track.id, storefront=playlist.storefront, url="", type=URLType.Song)
|
||||||
tg.create_task(
|
tg.create_task(
|
||||||
rip_song(song, auth_params, codec, config, device, force_save=force_save, playlist=playlist_info))
|
rip_song(song, auth_params, codec, config, device, song_status, force_save=force_save, playlist=playlist_info))
|
||||||
logger.info(
|
status.set_status(StatusCode.Done)
|
||||||
f"Playlist: {playlist_info.data[0].attributes.curatorName} - {playlist_info.data[0].attributes.name} finished ripping")
|
|
||||||
|
|
||||||
|
|
||||||
@logger.catch
|
@logger.catch
|
||||||
@timeit
|
@timeit
|
||||||
async def rip_artist(artist: Artist, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
|
async def rip_artist(artist: Artist, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device, status: BaseStatus,
|
||||||
force_save: bool = False, include_participate_in_works: bool = False):
|
force_save: bool = False, include_participate_in_works: bool = False):
|
||||||
artist_info = await get_artist_info(artist.id, artist.storefront, auth_params.anonymousAccessToken,
|
artist_info = await get_artist_info(artist.id, artist.storefront, auth_params.anonymousAccessToken,
|
||||||
config.region.language)
|
config.region.language)
|
||||||
logger.info(f"Ripping Artist: {artist_info.data[0].attributes.name}")
|
status.set_param(artist=artist_info.data[0].attributes.name)
|
||||||
|
status.set_status(StatusCode.Processing)
|
||||||
async with asyncio.TaskGroup() as tg:
|
async with asyncio.TaskGroup() as tg:
|
||||||
if include_participate_in_works:
|
if include_participate_in_works:
|
||||||
songs = await get_songs_from_artist(artist.id, artist.storefront, auth_params.anonymousAccessToken,
|
songs = await get_songs_from_artist(artist.id, artist.storefront, auth_params.anonymousAccessToken,
|
||||||
config.region.language)
|
config.region.language)
|
||||||
for song_url in songs:
|
for song_url in songs:
|
||||||
tg.create_task(rip_song(Song.parse_url(song_url), auth_params, codec, config, device, force_save))
|
song_status = status.new(URLType.Song)
|
||||||
|
status.children.append(song_status)
|
||||||
|
tg.create_task(rip_song(Song.parse_url(song_url), auth_params, codec, config, device, song_status, force_save=force_save))
|
||||||
else:
|
else:
|
||||||
albums = await get_albums_from_artist(artist.id, artist.storefront, auth_params.anonymousAccessToken,
|
albums = await get_albums_from_artist(artist.id, artist.storefront, auth_params.anonymousAccessToken,
|
||||||
config.region.language)
|
config.region.language)
|
||||||
for album_url in albums:
|
for album_url in albums:
|
||||||
tg.create_task(rip_album(Album.parse_url(album_url), auth_params, codec, config, device, force_save))
|
album_status = status.new(URLType.Song)
|
||||||
logger.info(f"Artist: {artist_info.data[0].attributes.name} finished ripping")
|
status.children.append(album_status)
|
||||||
|
tg.create_task(rip_album(Album.parse_url(album_url), auth_params, codec, config, device, album_status, force_save=force_save))
|
||||||
|
status.set_status(StatusCode.Done)
|
||||||
|
|||||||
158
src/status.py
Normal file
158
src/status.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from src.url import URLType
|
||||||
|
|
||||||
|
|
||||||
|
class StatusCode:
|
||||||
|
"""
|
||||||
|
For Song, available values are all.
|
||||||
|
For others, available values are Waiting, Processing, Done and Failed.
|
||||||
|
"""
|
||||||
|
Waiting = "WAITING"
|
||||||
|
Processing = "PROCESSING"
|
||||||
|
Parsing = "PARSING"
|
||||||
|
Downloading = "DOWNLOADING"
|
||||||
|
Decrypting = "DECRYPTING"
|
||||||
|
Saving = "SAVING"
|
||||||
|
Done = "Done"
|
||||||
|
AlreadyExist = "ALREADY_EXIST"
|
||||||
|
Failed = "FAILED"
|
||||||
|
|
||||||
|
|
||||||
|
class WarningCode:
|
||||||
|
NoAvailableAccountForLyrics = "NO_AVAILABLE_ACCOUNT_FOR_LYRICS"
|
||||||
|
UnableGetLyrics = "UNABLE_GET_LYRICS"
|
||||||
|
RetryableDecryptFailed = "RETRYABLE_DECRYPT_FAILED"
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorCode:
|
||||||
|
NotExistInStorefront = "NOT_EXIST_IN_STOREFRONT"
|
||||||
|
ForceModeM3U8NotExist = "FORCE_MODE_M3U8_NOT_EXIST"
|
||||||
|
AudioNotExist = "AUDIO_NOT_EXIST"
|
||||||
|
LosslessAudioNotExist = "LOSSLESS_AUDIO_NOT_EXIST"
|
||||||
|
DecryptFailed = "DECRYPT_FAILED"
|
||||||
|
|
||||||
|
|
||||||
|
class BaseStatus:
|
||||||
|
_type: str
|
||||||
|
_current: str = StatusCode.Waiting
|
||||||
|
_status_params: dict[str, Any] = {}
|
||||||
|
_params: dict[str, Any] = {}
|
||||||
|
_warning: str = ""
|
||||||
|
_error: str = ""
|
||||||
|
children = []
|
||||||
|
|
||||||
|
def __init__(self, status_type: str):
|
||||||
|
self._type = status_type
|
||||||
|
|
||||||
|
def new(self, status_type):
|
||||||
|
new_obj = deepcopy(self)
|
||||||
|
new_obj._type = status_type
|
||||||
|
new_obj._current = StatusCode
|
||||||
|
new_obj._status_params = {}
|
||||||
|
new_obj._params = {}
|
||||||
|
new_obj._warning = ""
|
||||||
|
new_obj._error = ""
|
||||||
|
new_obj.children = []
|
||||||
|
return new_obj
|
||||||
|
|
||||||
|
def running(self):
|
||||||
|
if self._error:
|
||||||
|
return False
|
||||||
|
if self._current == StatusCode.Waiting or self._current == StatusCode.Done or self._current == StatusCode.AlreadyExist:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_status(self, status: str, **kwargs):
|
||||||
|
self._current = status
|
||||||
|
|
||||||
|
def get_status(self) -> str:
|
||||||
|
return self._current
|
||||||
|
|
||||||
|
def set_warning(self, warning: str, **kwargs):
|
||||||
|
self._warning = warning
|
||||||
|
|
||||||
|
def get_warning(self):
|
||||||
|
return self._warning
|
||||||
|
|
||||||
|
def set_error(self, error: str, **kwargs):
|
||||||
|
self._error = error
|
||||||
|
self._current = StatusCode.Failed
|
||||||
|
|
||||||
|
def get_error(self):
|
||||||
|
return self._error
|
||||||
|
|
||||||
|
def set_progress(self, key: str, now: int, total: int, **kwargs):
|
||||||
|
self._status_params[key] = {"now": now, "total": total}
|
||||||
|
|
||||||
|
def get_progress(self, key: str) -> Optional[tuple[int, int]]:
|
||||||
|
if self._status_params.get(key):
|
||||||
|
return self._status_params[key]["now"], self._status_params[key]["total"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_param(self, **kwargs):
|
||||||
|
for param in kwargs.items():
|
||||||
|
self._params[param[0]] = param[1]
|
||||||
|
|
||||||
|
|
||||||
|
class LogStatus(BaseStatus):
|
||||||
|
def _get_song_name(self) -> str:
|
||||||
|
if self._params.get('title'):
|
||||||
|
return f"{self._params.get('artist')} - {self._params.get('title')}"
|
||||||
|
return self._params.get('artist')
|
||||||
|
|
||||||
|
def set_status(self, status: str, **kwargs):
|
||||||
|
super().set_status(status, **kwargs)
|
||||||
|
match status:
|
||||||
|
case StatusCode.Waiting:
|
||||||
|
pass
|
||||||
|
case StatusCode.Processing:
|
||||||
|
if self._type == URLType.Song:
|
||||||
|
logger.debug(f"Task of {self._type} id {self._params.get('song_id')} was created")
|
||||||
|
else:
|
||||||
|
logger.info(f"Ripping {self._type}: {self._get_song_name()}")
|
||||||
|
case StatusCode.Parsing:
|
||||||
|
logger.info(f"Ripping {self._type}: {self._get_song_name()}")
|
||||||
|
case StatusCode.Downloading:
|
||||||
|
logger.info(f"Downloading {self._type}: {self._get_song_name()}")
|
||||||
|
case StatusCode.Decrypting:
|
||||||
|
logger.info(f"Decrypting {self._type}: {self._get_song_name()}")
|
||||||
|
case StatusCode.Saving:
|
||||||
|
pass
|
||||||
|
case StatusCode.Done:
|
||||||
|
logger.info(
|
||||||
|
f"{self._type.capitalize()} {self._get_song_name()} saved!")
|
||||||
|
case StatusCode.AlreadyExist:
|
||||||
|
logger.info(
|
||||||
|
f"{self._type.capitalize()}: {self._get_song_name()} already exists")
|
||||||
|
|
||||||
|
def set_warning(self, warning: str, **kwargs):
|
||||||
|
super().set_warning(warning, **kwargs)
|
||||||
|
match warning:
|
||||||
|
case WarningCode.NoAvailableAccountForLyrics:
|
||||||
|
logger.warning(f"No account is available for getting lyrics of storefront {self._params.get('song_storefront').upper()}. "
|
||||||
|
f"Use storefront {self._params.get('storefront').upper()} to get lyrics")
|
||||||
|
case WarningCode.RetryableDecryptFailed:
|
||||||
|
logger.warning(f"Failed to decrypt song: {self._get_song_name()}, {kwargs['action']}")
|
||||||
|
case WarningCode.UnableGetLyrics:
|
||||||
|
logger.warning(f"Unable to get lyrics of song: {self._get_song_name()}")
|
||||||
|
|
||||||
|
def set_error(self, error: str, **kwargs):
|
||||||
|
super().set_error(error, **kwargs)
|
||||||
|
match error:
|
||||||
|
case ErrorCode.AudioNotExist:
|
||||||
|
logger.error(f"Failed to download song: {self._get_song_name()}. Audio does not exist")
|
||||||
|
case ErrorCode.LosslessAudioNotExist:
|
||||||
|
logger.error(f"Failed to download song: {self._get_song_name()}. Lossless audio does not exist")
|
||||||
|
case ErrorCode.DecryptFailed:
|
||||||
|
logger.error(f"Failed to decrypt song: {self._get_song_name()}")
|
||||||
|
case ErrorCode.NotExistInStorefront:
|
||||||
|
logger.error(
|
||||||
|
f"Unable to download {self._type} {self._get_song_name()}. "
|
||||||
|
f"This {self._type} does not exist in storefront {self._params.get('storefront').upper()} "
|
||||||
|
f"and no device is available to decrypt it")
|
||||||
|
case ErrorCode.ForceModeM3U8NotExist:
|
||||||
|
logger.error(f"Failed to get m3u8 from API for song: {self._get_song_name()}")
|
||||||
Reference in New Issue
Block a user