|
|
@ -5,6 +5,7 @@ from ssl import SSLError |
|
|
|
import httpcore |
|
|
|
import httpcore |
|
|
|
import httpx |
|
|
|
import httpx |
|
|
|
import regex |
|
|
|
import regex |
|
|
|
|
|
|
|
from async_lru import alru_cache |
|
|
|
from loguru import logger |
|
|
|
from loguru import logger |
|
|
|
from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log |
|
|
|
from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log |
|
|
|
|
|
|
|
|
|
|
@ -12,22 +13,25 @@ from src.models import * |
|
|
|
from src.models.song_data import Datum |
|
|
|
from src.models.song_data import Datum |
|
|
|
|
|
|
|
|
|
|
|
client: httpx.AsyncClient |
|
|
|
client: httpx.AsyncClient |
|
|
|
lock: asyncio.Semaphore |
|
|
|
download_lock: asyncio.Semaphore |
|
|
|
|
|
|
|
request_lock: asyncio.Semaphore |
|
|
|
user_agent_browser = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" |
|
|
|
user_agent_browser = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" |
|
|
|
user_agent_itunes = "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professional Edition (Build 19041); x64) AppleWebKit/7611.1022.4001.1 (dt:2)" |
|
|
|
user_agent_itunes = "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professional Edition (Build 19041); x64) AppleWebKit/7611.1022.4001.1 (dt:2)" |
|
|
|
user_agent_app = "Music/5.7 Android/10 model/Pixel6GR1YH build/1234 (dt:66)" |
|
|
|
user_agent_app = "Music/5.7 Android/10 model/Pixel6GR1YH build/1234 (dt:66)" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_client_and_lock(proxy: str, parallel_num: int): |
|
|
|
def init_client_and_lock(proxy: str, parallel_num: int): |
|
|
|
global client, lock |
|
|
|
global client, download_lock, request_lock |
|
|
|
if proxy: |
|
|
|
if proxy: |
|
|
|
client = httpx.AsyncClient(proxy=proxy) |
|
|
|
client = httpx.AsyncClient(proxy=proxy) |
|
|
|
else: |
|
|
|
else: |
|
|
|
client = httpx.AsyncClient() |
|
|
|
client = httpx.AsyncClient() |
|
|
|
lock = asyncio.Semaphore(parallel_num) |
|
|
|
download_lock = asyncio.Semaphore(parallel_num) |
|
|
|
|
|
|
|
request_lock = asyncio.Semaphore(64) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_m3u8_from_api(endpoint: str, song_id: str) -> str: |
|
|
|
async def get_m3u8_from_api(endpoint: str, song_id: str) -> str: |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
resp = (await client.get(endpoint, params={"songid": song_id})).text |
|
|
|
resp = (await client.get(endpoint, params={"songid": song_id})).text |
|
|
|
if resp == "no_found": |
|
|
|
if resp == "no_found": |
|
|
|
return "" |
|
|
|
return "" |
|
|
@ -35,6 +39,7 @@ async def get_m3u8_from_api(endpoint: str, song_id: str) -> str: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def upload_m3u8_to_api(endpoint: str, m3u8_url: str, song_info: Datum): |
|
|
|
async def upload_m3u8_to_api(endpoint: str, m3u8_url: str, song_info: Datum): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
await client.post(endpoint, json={ |
|
|
|
await client.post(endpoint, json={ |
|
|
|
"method": "add_m3u8", |
|
|
|
"method": "add_m3u8", |
|
|
|
"params": { |
|
|
|
"params": { |
|
|
@ -51,6 +56,7 @@ async def upload_m3u8_to_api(endpoint: str, m3u8_url: str, song_info: Datum): |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def get_token(): |
|
|
|
async def get_token(): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
req = await client.get("https://beta.music.apple.com") |
|
|
|
req = await client.get("https://beta.music.apple.com") |
|
|
|
index_js_uri = regex.findall(r"/assets/index-legacy-[^/]+\.js", req.text)[0] |
|
|
|
index_js_uri = regex.findall(r"/assets/index-legacy-[^/]+\.js", req.text)[0] |
|
|
|
js_req = await client.get("https://beta.music.apple.com" + index_js_uri) |
|
|
|
js_req = await client.get("https://beta.music.apple.com" + index_js_uri) |
|
|
@ -58,18 +64,23 @@ async def get_token(): |
|
|
|
return token |
|
|
|
return token |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, |
|
|
|
|
|
|
|
httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def download_song(url: str) -> bytes: |
|
|
|
async def download_song(url: str) -> bytes: |
|
|
|
async with lock: |
|
|
|
async with download_lock: |
|
|
|
return (await client.get(url)).content |
|
|
|
return (await client.get(url)).content |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def get_album_info(album_id: str, token: str, storefront: str, lang: str): |
|
|
|
async def get_album_info(album_id: str, token: str, storefront: str, lang: str): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/albums/{album_id}", |
|
|
|
req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/albums/{album_id}", |
|
|
|
params={"omit[resource]": "autos", "include": "tracks,artists,record-labels", |
|
|
|
params={"omit[resource]": "autos", "include": "tracks,artists,record-labels", |
|
|
|
"include[songs]": "artists", "fields[artists]": "name", |
|
|
|
"include[songs]": "artists", "fields[artists]": "name", |
|
|
@ -80,10 +91,13 @@ async def get_album_info(album_id: str, token: str, storefront: str, lang: str): |
|
|
|
return AlbumMeta.model_validate(req.json()) |
|
|
|
return AlbumMeta.model_validate(req.json()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def get_playlist_info_and_tracks(playlist_id: str, token: str, storefront: str, lang: str): |
|
|
|
async def get_playlist_info_and_tracks(playlist_id: str, token: str, storefront: str, lang: str): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/playlists/{playlist_id}", |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/playlists/{playlist_id}", |
|
|
|
params={"l": lang}, |
|
|
|
params={"l": lang}, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
@ -95,11 +109,15 @@ async def get_playlist_info_and_tracks(playlist_id: str, token: str, storefront: |
|
|
|
return playlist_info_obj |
|
|
|
return playlist_info_obj |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def get_playlist_tracks(playlist_id: str, token: str, storefront: str, lang: str, offset: int = 0): |
|
|
|
async def get_playlist_tracks(playlist_id: str, token: str, storefront: str, lang: str, offset: int = 0): |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/playlists/{playlist_id}/tracks", |
|
|
|
async with request_lock: |
|
|
|
|
|
|
|
resp = await client.get( |
|
|
|
|
|
|
|
f"https://amp-api.music.apple.com/v1/catalog/{storefront}/playlists/{playlist_id}/tracks", |
|
|
|
params={"l": lang, "offset": offset}, |
|
|
|
params={"l": lang, "offset": offset}, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
|
"Origin": "https://music.apple.com"}) |
|
|
|
"Origin": "https://music.apple.com"}) |
|
|
@ -111,20 +129,26 @@ async def get_playlist_tracks(playlist_id: str, token: str, storefront: str, lan |
|
|
|
return tracks |
|
|
|
return tracks |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def get_cover(url: str, cover_format: str, cover_size: str): |
|
|
|
async def get_cover(url: str, cover_format: str, cover_size: str): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
formatted_url = regex.sub('bb.jpg', f'bb.{cover_format}', url) |
|
|
|
formatted_url = regex.sub('bb.jpg', f'bb.{cover_format}', url) |
|
|
|
req = await client.get(formatted_url.replace("{w}x{h}", cover_size), |
|
|
|
req = await client.get(formatted_url.replace("{w}x{h}", cover_size), |
|
|
|
headers={"User-Agent": user_agent_browser}) |
|
|
|
headers={"User-Agent": user_agent_browser}) |
|
|
|
return req.content |
|
|
|
return req.content |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def get_song_info(song_id: str, token: str, storefront: str, lang: str): |
|
|
|
async def get_song_info(song_id: str, token: str, storefront: str, lang: str): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{song_id}", |
|
|
|
req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{song_id}", |
|
|
|
params={"extend": "extendedAssetUrls", "include": "albums", "l": lang}, |
|
|
|
params={"extend": "extendedAssetUrls", "include": "albums", "l": lang}, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_itunes, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_itunes, |
|
|
@ -136,10 +160,13 @@ async def get_song_info(song_id: str, token: str, storefront: str, lang: str): |
|
|
|
return None |
|
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def get_song_lyrics(song_id: str, storefront: str, token: str, dsid: str, account_token: str, lang: str) -> str: |
|
|
|
async def get_song_lyrics(song_id: str, storefront: str, token: str, dsid: str, account_token: str, lang: str) -> str: |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{song_id}/lyrics", |
|
|
|
req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{song_id}/lyrics", |
|
|
|
params={"l": lang}, |
|
|
|
params={"l": lang}, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_app, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_app, |
|
|
@ -149,10 +176,13 @@ async def get_song_lyrics(song_id: str, storefront: str, token: str, dsid: str, |
|
|
|
return result.data[0].attributes.ttml |
|
|
|
return result.data[0].attributes.ttml |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
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): |
|
|
|
async def get_albums_from_artist(artist_id: str, storefront: str, token: str, lang: str, offset: int = 0): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}/albums", |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}/albums", |
|
|
|
params={"l": lang, "offset": offset}, |
|
|
|
params={"l": lang, "offset": offset}, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
@ -165,10 +195,13 @@ async def get_albums_from_artist(artist_id: str, storefront: str, token: str, la |
|
|
|
return list(set(albums)) |
|
|
|
return list(set(albums)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
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): |
|
|
|
async def get_songs_from_artist(artist_id: str, storefront: str, token: str, lang: str, offset: int = 0): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}/songs", |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}/songs", |
|
|
|
params={"l": lang, "offset": offset}, |
|
|
|
params={"l": lang, "offset": offset}, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
@ -181,12 +214,25 @@ async def get_songs_from_artist(artist_id: str, storefront: str, token: str, lan |
|
|
|
return list[set(songs)] |
|
|
|
return list[set(songs)] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), |
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
stop=stop_after_attempt(5), |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
async def get_artist_info(artist_id: str, storefront: str, token: str, lang: str): |
|
|
|
async def get_artist_info(artist_id: str, storefront: str, token: str, lang: str): |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}", |
|
|
|
resp = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/artists/{artist_id}", |
|
|
|
params={"l": lang}, |
|
|
|
params={"l": lang}, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
|
headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser, |
|
|
|
"Origin": "https://music.apple.com"}) |
|
|
|
"Origin": "https://music.apple.com"}) |
|
|
|
return ArtistInfo.parse_obj(resp.json()) |
|
|
|
return ArtistInfo.parse_obj(resp.json()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@alru_cache |
|
|
|
|
|
|
|
@retry(retry=retry_if_exception_type( |
|
|
|
|
|
|
|
(httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError, httpcore.RemoteProtocolError)), |
|
|
|
|
|
|
|
stop=stop_after_attempt(5), |
|
|
|
|
|
|
|
before_sleep=before_sleep_log(logger, logging.WARN)) |
|
|
|
|
|
|
|
async def download_m3u8(m3u8_url: str) -> str: |
|
|
|
|
|
|
|
async with request_lock: |
|
|
|
|
|
|
|
return (await client.get(m3u8_url)).text |
|
|
|