Skip to content

Commit

Permalink
feat: download artist
Browse files Browse the repository at this point in the history
  • Loading branch information
WorldObservationLog committed May 6, 2024
1 parent 8dda246 commit fc53f21
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 7 deletions.
43 changes: 43 additions & 0 deletions src/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,46 @@ async def get_song_lyrics(song_id: str, storefront: str, token: str, dsid: str,
cookies={f"mz_at_ssl-{dsid}": account_token})
result = SongLyrics.model_validate(req.json())
return result.data[0].attributes.ttml


@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def get_albums_from_artist(artist_id: str, storefront: str, token: str, lang: str, offset: int = 0):
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}/albums",
params={"l": lang},
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser,
"Origin": "https://music.apple.com"})
artist_album = ArtistAlbums.parse_obj(resp.json())
albums = [album.attributes.url for album in artist_album.data]
if artist_album.next:
next_albums = await get_albums_from_artist(artist_id, storefront, token, lang, offset + 25)
albums.extend(next_albums)
return list(set(albums))


@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def get_songs_from_artist(artist_id: str, storefront: str, token: str, lang: str, offset: int = 0):
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}/songs",
params={"l": lang},
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser,
"Origin": "https://music.apple.com"})
artist_song = ArtistSongs.parse_obj(resp.json())
songs = [song.attributes.url for song in artist_song.data]
if artist_song.next:
next_songs = await get_songs_from_artist(artist_id, storefront, token, lang, offset + 20)
songs.extend(next_songs)
return list[set(songs)]


@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def get_artist_info(artist_id: str, storefront: str, token: str, lang: str):
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}",
params={"l": lang},
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser,
"Origin": "https://music.apple.com"})
return ArtistInfo.parse_obj(resp.json())
16 changes: 13 additions & 3 deletions src/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from src.adb import Device
from src.api import get_token, init_client_and_lock, upload_m3u8_to_api, get_info_from_adam
from src.config import Config
from src.rip import rip_song, rip_album
from src.rip import rip_song, rip_album, rip_artist
from src.types import GlobalAuthParams
from src.url import AppleMusicURL, URLType, Song
from src.utils import get_song_id_from_m3u8
Expand Down Expand Up @@ -41,6 +41,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop):
choices=["alac", "ec3", "aac", "aac-binaural", "aac-downmix", "ac3"],
default="alac")
download_parser.add_argument("-f", "--force", type=bool, default=False)
download_parser.add_argument("--include-participate-songs", type=bool, default=False, dest="include")
m3u8_parser = subparser.add_parser("m3u8")
m3u8_parser.add_argument("url", type=str)
m3u8_parser.add_argument("-c", "--codec",
Expand Down Expand Up @@ -79,7 +80,7 @@ async def command_parser(self, cmd: str):
return
match cmds[0]:
case "download":
await self.do_download(args.url, args.codec, args.force)
await self.do_download(args.url, args.codec, args.force, args.include)
case "m3u8":
await self.do_m3u8(args.url, args.codec, args.force)
case "mitm":
Expand All @@ -88,17 +89,26 @@ async def command_parser(self, cmd: str):
self.loop.stop()
sys.exit()

async def do_download(self, raw_url: str, codec: str, force_download: bool):
async def do_download(self, raw_url: str, codec: str, force_download: bool, include: bool = False):
url = AppleMusicURL.parse_url(raw_url)
available_device = await self._get_available_device(url.storefront)
global_auth_param = GlobalAuthParams.from_auth_params_and_token(available_device.get_auth_params(),
self.anonymous_access_token)
tasks = set()
match url.type:
case URLType.Song:
task = self.loop.create_task(
rip_song(url, global_auth_param, codec, self.config, available_device, force_download))
case URLType.Album:
task = self.loop.create_task(rip_album(url, global_auth_param, codec, self.config, available_device))
case URLType.Artist:
task = self.loop.create_task(rip_artist(url, global_auth_param, codec, self.config, available_device,
force_download, include))
case _:
logger.error("Unsupported URLType")
return
tasks.add(task)
task.add_done_callback(tasks.remove)

async def do_m3u8(self, m3u8_url: str, codec: str, force_download: bool):
song_id = get_song_id_from_m3u8(m3u8_url)
Expand Down
3 changes: 3 additions & 0 deletions src/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
from src.models.song_data import SongData
from src.models.song_lyrics import SongLyrics
from src.models.tracks_meta import TracksMeta
from src.models.artist_albums import ArtistAlbums
from src.models.artist_songs import ArtistSongs
from src.models.artist_info import ArtistInfo
71 changes: 71 additions & 0 deletions src/models/artist_albums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

from typing import List, Optional

from pydantic import BaseModel


class Artwork(BaseModel):
width: Optional[int] = None
url: Optional[str] = None
height: Optional[int] = None
textColor3: Optional[str] = None
textColor2: Optional[str] = None
textColor4: Optional[str] = None
textColor1: Optional[str] = None
bgColor: Optional[str] = None
hasP3: bool


class PlayParams(BaseModel):
id: Optional[str] = None
kind: Optional[str] = None


class EditorialNotes(BaseModel):
short: Optional[str] = None
standard: Optional[str] = None
name: Optional[str] = None


class Attributes(BaseModel):
copyright: Optional[str] = None
genreNames: List[str]
releaseDate: Optional[str] = None
isMasteredForItunes: bool
upc: Optional[str] = None
artwork: Artwork
url: Optional[str] = None
playParams: PlayParams
recordLabel: Optional[str] = None
trackCount: Optional[int] = None
isCompilation: bool
isPrerelease: bool
audioTraits: List[str]
isSingle: bool
name: Optional[str] = None
artistName: Optional[str] = None
isComplete: bool
editorialNotes: Optional[EditorialNotes] = None


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
meta: Meta


class ArtistAlbums(BaseModel):
next: Optional[str] = None
data: List[Datum]
53 changes: 53 additions & 0 deletions src/models/artist_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

from typing import List, Optional

from pydantic import BaseModel


class Artwork(BaseModel):
width: Optional[int] = None
url: Optional[str] = None
height: Optional[int] = None
textColor3: Optional[str] = None
textColor2: Optional[str] = None
textColor4: Optional[str] = None
textColor1: Optional[str] = None
bgColor: Optional[str] = None
hasP3: bool


class Attributes(BaseModel):
genreNames: List[Optional[str]] = None
name: Optional[str] = None
artwork: Artwork
classicalUrl: Optional[str] = None
url: Optional[str] = None


class Datum1(BaseModel):
id: Optional[str] = None
type: Optional[str] = None
href: Optional[str] = None


class Albums(BaseModel):
href: Optional[str] = None
next: Optional[str] = None
data: List[Datum1]


class Relationships(BaseModel):
albums: Albums


class Datum(BaseModel):
id: Optional[str] = None
type: Optional[str] = None
href: Optional[str] = None
attributes: Attributes
relationships: Relationships


class ArtistInfo(BaseModel):
data: List[Datum]
73 changes: 73 additions & 0 deletions src/models/artist_songs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from __future__ import annotations

from typing import List, Optional

from pydantic import BaseModel


class Artwork(BaseModel):
width: Optional[int] = None
url: Optional[str] = None
height: Optional[int] = None
textColor3: Optional[str] = None
textColor2: Optional[str] = None
textColor4: Optional[str] = None
textColor1: Optional[str] = None
bgColor: Optional[str] = None
hasP3: bool


class PlayParams(BaseModel):
id: Optional[str] = None
kind: Optional[str] = None


class Preview(BaseModel):
url: Optional[str] = None


class Attributes(BaseModel):
hasTimeSyncedLyrics: bool
albumName: Optional[str] = None
genreNames: List[str]
trackNumber: Optional[int] = None
releaseDate: Optional[str] = None
durationInMillis: Optional[int] = None
isVocalAttenuationAllowed: bool
isMasteredForItunes: bool
isrc: Optional[str] = None
artwork: Artwork
audioLocale: Optional[str] = None
composerName: Optional[str] = None
url: Optional[str] = None
playParams: PlayParams
discNumber: Optional[int] = None
hasCredits: bool
hasLyrics: bool
isAppleDigitalMaster: bool
audioTraits: List[str]
name: Optional[str] = None
previews: List[Preview]
artistName: Optional[str] = None


class ContentVersion(BaseModel):
RTCI: Optional[int] = None
MZ_INDEXER: 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
meta: Meta


class ArtistSongs(BaseModel):
next: Optional[str] = None
data: List[Datum]
21 changes: 17 additions & 4 deletions src/rip.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@

from loguru import logger

from src.api import get_info_from_adam, get_song_lyrics, get_meta, download_song, get_m3u8_from_api
from src.api import (get_info_from_adam, get_song_lyrics, get_meta, download_song,
get_m3u8_from_api, get_artist_info, get_songs_from_artist, get_albums_from_artist)
from src.config import Config, Device
from src.decrypt import decrypt
from src.metadata import SongMetadata
from src.mp4 import extract_media, extract_song, encapsulate, write_metadata
from src.save import save
from src.types import GlobalAuthParams, Codec
from src.url import Song, Album, URLType
from src.url import Song, Album, URLType, Artist
from src.utils import check_song_exists


Expand Down Expand Up @@ -74,5 +75,17 @@ async def rip_playlist():
pass


async def rip_artist():
pass
async def rip_artist(artist: Artist, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
force_save: bool = False, include_participate_in_works: bool = False):
artist_info = await get_artist_info(artist.id, artist.storefront, auth_params.anonymousAccessToken, config.region.language)
logger.info(f"Ripping Artist: {artist_info.data[0].attributes.name}")
async with asyncio.TaskGroup() as tg:
if include_participate_in_works:
songs = await get_songs_from_artist(artist.id, artist.storefront, auth_params.anonymousAccessToken, config.region.language)
for song_url in songs:
tg.create_task(rip_song(Song.parse_url(song_url), auth_params, codec, config, device, force_save))
else:
albums = await get_albums_from_artist(artist.id, artist.storefront, auth_params.anonymousAccessToken, config.region.language)
for album_url in albums:
tg.create_task(rip_album(Album.parse_url(album_url), auth_params, codec, config, device, force_save))
logger.info(f"Artist: {artist_info.data[0].attributes.name} finished ripping")

0 comments on commit fc53f21

Please sign in to comment.