Update to wrapper-v2 endpoints

This commit is contained in:
Rafael Moraes
2026-05-18 13:42:30 -03:00
parent 2205b76c07
commit e44b037414
8 changed files with 2177 additions and 2191 deletions
+2 -2
View File
@@ -242,14 +242,14 @@ class AppleMusicApi:
@classmethod
async def create_from_wrapper(
cls,
wrapper_account_url: str = "http://127.0.0.1:80/me",
wrapper_url: str = "http://127.0.0.1",
*args,
**kwargs,
) -> "AppleMusicApi":
response = None
async with httpx.AsyncClient() as client:
try:
response = await client.get(wrapper_account_url)
response = await client.get(wrapper_url + "/me")
response.raise_for_status()
wrapper_account_info = response.json()
except httpx.HTTPError:
+2 -3
View File
@@ -79,7 +79,7 @@ async def main(config: CliConfig):
if config.use_wrapper:
try:
apple_music_api = await AppleMusicApi.create_from_wrapper(
wrapper_account_url=config.wrapper_account_url,
wrapper_url=config.wrapper_url,
language=config.language,
)
except ConnectError:
@@ -134,7 +134,7 @@ async def main(config: CliConfig):
cover_format=config.cover_format,
cover_size=config.cover_size,
use_wrapper=config.use_wrapper,
wrapper_m3u8_ip=config.wrapper_m3u8_ip,
wrapper_url=config.wrapper_url,
wvd_path=config.wvd_path,
)
@@ -176,7 +176,6 @@ async def main(config: CliConfig):
mp4decrypt_path=config.mp4decrypt_path,
ffmpeg_path=config.ffmpeg_path,
mp4box_path=config.mp4box_path,
wrapper_decrypt_url=config.wrapper_decrypt_url,
download_mode=config.download_mode,
album_folder_template=config.album_folder_template,
compilation_folder_template=config.compilation_folder_template,
+4 -20
View File
@@ -161,12 +161,12 @@ class CliConfig:
),
),
]
wrapper_account_url: Annotated[
wrapper_url: Annotated[
str,
option(
"--wrapper-account-url",
help="Wrapper account URL",
default=api_from_wrapper_sig.parameters["wrapper_account_url"].default,
"--wrapper-url",
help="Wrapper URL",
default=api_from_wrapper_sig.parameters["wrapper_url"].default,
),
]
language: Annotated[
@@ -218,14 +218,6 @@ class CliConfig:
is_flag=True,
),
]
wrapper_m3u8_ip: Annotated[
str,
option(
"--wrapper-m3u8-ip",
help="Wrapper m3u8 IP address and port",
default=base_interface_create_sig.parameters["wrapper_m3u8_ip"].default,
),
]
# Song Interface Options
synced_lyrics_format: Annotated[
SyncedLyricsFormat,
@@ -344,14 +336,6 @@ class CliConfig:
default=base_downloader_sig.parameters["mp4box_path"].default,
),
]
wrapper_decrypt_url: Annotated[
str,
option(
"--wrapper-decrypt-url",
help="wrapper-v2 base URL for FairPlay decrypt (e.g. http://127.0.0.1:80 or host:port)",
default=base_downloader_sig.parameters["wrapper_decrypt_url"].default,
),
]
download_mode: Annotated[
DownloadMode,
option(
+2071 -2071
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -27,7 +27,6 @@ class AppleMusicBaseDownloader:
mp4decrypt_path: str = "mp4decrypt",
ffmpeg_path: str = "ffmpeg",
mp4box_path: str = "MP4Box",
wrapper_decrypt_url: str = "http://127.0.0.1:80",
download_mode: DownloadMode = DownloadMode.YTDLP,
album_folder_template: str = "{album_artist}/{album}",
compilation_folder_template: str = "Compilations/{album}",
@@ -49,7 +48,6 @@ class AppleMusicBaseDownloader:
self.mp4decrypt_path = mp4decrypt_path
self.ffmpeg_path = ffmpeg_path
self.mp4box_path = mp4box_path
self.wrapper_decrypt_url = wrapper_decrypt_url
self.download_mode = download_mode
self.album_folder_template = album_folder_template
self.compilation_folder_template = compilation_folder_template
+1 -1
View File
@@ -59,7 +59,7 @@ class AppleMusicSongDownloader:
fairplay_key: str,
) -> None:
await decrypt_file(
self.base.wrapper_decrypt_url,
self.base.interface.base.wrapper_url + "/decrypt",
media_id,
fairplay_key,
input_path,
+4 -4
View File
@@ -30,7 +30,7 @@ class AppleMusicBaseInterface:
cover_format: CoverFormat,
cover_size: int,
use_wrapper: bool,
wrapper_m3u8_ip: str,
wrapper_url: str,
cdm: Cdm,
) -> None:
self.apple_music_api = apple_music_api
@@ -38,7 +38,7 @@ class AppleMusicBaseInterface:
self.cover_format = cover_format
self.cover_size = cover_size
self.use_wrapper = use_wrapper
self.wrapper_m3u8_ip = wrapper_m3u8_ip
self.wrapper_url = wrapper_url
self.cdm = cdm
@staticmethod
@@ -126,7 +126,7 @@ class AppleMusicBaseInterface:
cover_format: CoverFormat = CoverFormat.JPG,
cover_size: int = 1200,
use_wrapper: bool = False,
wrapper_m3u8_ip: str = "127.0.0.1:20020",
wrapper_url: str = "http://127.0.0.1",
wvd_path: str | None = None,
itunes_api: ItunesApi | None = None,
):
@@ -147,7 +147,7 @@ class AppleMusicBaseInterface:
cover_format=cover_format,
cover_size=cover_size,
use_wrapper=use_wrapper,
wrapper_m3u8_ip=wrapper_m3u8_ip,
wrapper_url=wrapper_url,
cdm=cdm,
)
return base
+93 -88
View File
@@ -28,6 +28,7 @@ from .types import (
StreamInfo,
StreamInfoAv,
)
import httpx
logger = structlog.get_logger(__name__)
@@ -189,100 +190,74 @@ class AppleMusicSongInterface:
return f"[{timestamp.strftime('%M:%S.%f')[:-4]}]{text}"
async def _get_wrapper_playback(self, media_id: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base.wrapper_url}/playback",
params={"adam_id": media_id},
)
response.raise_for_status()
return response.json()
def _get_m3u8_from_playback(self, playback: dict) -> str | None:
return playback["songList"][0].get("hls-playlist-url")
async def get_tags(
self,
webplayback: dict,
asset_data: dict,
lyrics: str | None = None,
) -> MediaTags:
log = logger.bind(action="get_song_tags")
webplayback_metadata = webplayback["songList"][0]["assets"][0]["metadata"]
tags = MediaTags(
album=webplayback_metadata["playlistName"],
album_artist=webplayback_metadata["playlistArtistName"],
album_id=int(webplayback_metadata["playlistId"]),
album_sort=webplayback_metadata["sort-album"],
artist=webplayback_metadata["artistName"],
artist_id=int(webplayback_metadata["artistId"]),
artist_sort=webplayback_metadata["sort-artist"],
comment=webplayback_metadata.get("comments"),
compilation=webplayback_metadata["compilation"],
composer=webplayback_metadata.get("composerName"),
album=asset_data["playlistName"],
album_artist=asset_data["playlistArtistName"],
album_id=int(asset_data["playlistId"]),
album_sort=asset_data["sort-album"],
artist=asset_data["artistName"],
artist_id=int(asset_data["artistId"]),
artist_sort=asset_data["sort-artist"],
comment=asset_data.get("comments"),
compilation=asset_data["compilation"],
composer=asset_data.get("composerName"),
composer_id=(
int(webplayback_metadata.get("composerId"))
if webplayback_metadata.get("composerId")
int(asset_data.get("composerId"))
if asset_data.get("composerId")
else None
),
composer_sort=webplayback_metadata.get("sort-composer"),
copyright=webplayback_metadata.get("copyright"),
composer_sort=asset_data.get("sort-composer"),
copyright=asset_data.get("copyright"),
date=(
await self.base.get_media_date(webplayback_metadata["playlistId"])
await self.base.get_media_date(asset_data["playlistId"])
if self.use_album_date
else (
self.base.parse_date(webplayback_metadata["releaseDate"])
if webplayback_metadata.get("releaseDate")
self.base.parse_date(asset_data["releaseDate"])
if asset_data.get("releaseDate")
else None
)
),
disc=webplayback_metadata["discNumber"],
disc_total=webplayback_metadata["discCount"],
gapless=webplayback_metadata["gapless"],
genre=webplayback_metadata.get("genre"),
genre_id=int(webplayback_metadata["genreId"]),
disc=asset_data["discNumber"],
disc_total=asset_data["discCount"],
gapless=asset_data["gapless"],
genre=asset_data.get("genre"),
genre_id=int(asset_data["genreId"]),
lyrics=lyrics if lyrics else None,
media_type=MediaType.SONG,
rating=MediaRating(webplayback_metadata["explicit"]),
storefront=webplayback_metadata["s"],
title=webplayback_metadata["itemName"],
title_id=int(webplayback_metadata["itemId"]),
title_sort=webplayback_metadata["sort-name"],
track=webplayback_metadata["trackNumber"],
track_total=webplayback_metadata["trackCount"],
xid=webplayback_metadata.get("xid"),
rating=MediaRating(asset_data["explicit"]),
storefront=asset_data["s"],
title=asset_data["itemName"],
title_id=int(asset_data["itemId"]),
title_sort=asset_data["sort-name"],
track=asset_data["trackNumber"],
track_total=asset_data["trackCount"],
xid=asset_data.get("xid"),
)
log.debug("success", tags=tags)
return tags
async def get_stream_info(
self,
song_metadata: dict | None = None,
webplayback: dict | None = None,
) -> StreamInfoAv | None:
for codec in self.codec_priority:
if codec.is_legacy():
return await self._get_stream_info_legacy(webplayback, codec)
else:
return await self._get_stream_info(song_metadata, codec)
async def get_wrapper_m3u8(self, adam_id: str) -> str | None:
host, port = self.base.wrapper_m3u8_ip.split(":")
reader, writer = await asyncio.open_connection(host, port)
data = struct.pack("B", len(adam_id)) + adam_id.encode()
writer.write(data)
await writer.drain()
response = await reader.readuntil(b"\n")
m3u8_url = response.decode().strip()
writer.close()
await writer.wait_closed()
if m3u8_url:
return m3u8_url
return None
async def _get_stream_info(
self,
song_metadata: dict,
codec: SongCodec,
) -> StreamInfoAv | None:
log = logger.bind(action="get_song_stream_info")
async def _get_m3u8_from_metadata(self, song_metadata: dict) -> str | None:
if "extendedAssetUrls" not in song_metadata["attributes"]:
song_metadata = (
await self.base.apple_music_api.get_song(
@@ -290,10 +265,28 @@ class AppleMusicSongInterface:
)
)["data"][0]
m3u8_master_url = song_metadata["attributes"]["extendedAssetUrls"].get(
"enhancedHls"
)
return song_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls")
async def get_stream_info(
self,
m3u8_master_url: str | None = None,
webplayback: dict | None = None,
) -> StreamInfoAv | None:
for codec in self.codec_priority:
if codec.is_legacy():
return await self._get_stream_info_legacy(webplayback, codec)
else:
return await self._get_stream_info(m3u8_master_url, codec)
async def _get_stream_info(
self,
m3u8_master_url: str | None,
codec: SongCodec,
) -> StreamInfoAv | None:
log = logger.bind(action="get_song_stream_info")
if not m3u8_master_url:
log.debug("no_m3u8_master_url")
return None
m3u8_master_obj = m3u8.loads(
@@ -499,24 +492,36 @@ class AppleMusicSongInterface:
media.lyrics = await self.get_lyrics(media.media_metadata)
webplayback = await self.base.apple_music_api.get_webplayback(media.media_id)
media.tags = await self.get_tags(
webplayback,
media.lyrics.unsynced if media.lyrics else None,
)
if not self.skip_stream_info:
media.stream_info = await self.get_stream_info(
media.media_metadata,
webplayback,
if self.base.use_wrapper:
playback = await self._get_wrapper_playback(media.media_id)
media.tags = await self.get_tags(
playback["songList"][0]["assets"][0]["metadata"],
media.lyrics.unsynced if media.lyrics else None,
)
if not media.stream_info:
raise GamdlInterfaceFormatNotAvailableError(
media_id=media.media_id,
codec=self.codec_priority,
if not self.skip_stream_info:
m3u8_master_url = self._get_m3u8_from_playback(playback)
media.stream_info = await self.get_stream_info(
m3u8_master_url,
playback,
)
else:
webplayback = await self.base.apple_music_api.get_webplayback(
media.media_id
)
media.tags = await self.get_tags(
webplayback["songList"][0]["assets"][0]["metadata"],
media.lyrics.unsynced if media.lyrics else None,
)
if not self.skip_stream_info:
m3u8_master_url = await self._get_m3u8_from_metadata(
media.media_metadata
)
media.stream_info = await self.get_stream_info(
m3u8_master_url,
webplayback,
)
if media.stream_info:
if (
not self.base.use_wrapper
and not media.stream_info.audio_track.widevine_pssh