mirror of
https://github.com/glomatico/gamdl.git
synced 2026-06-13 04:05:14 +03:00
Update to wrapper-v2 endpoints
This commit is contained in:
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user