Compare commits

...

10 Commits

Author SHA1 Message Date
glomatico b0c5335767 Bump version to 3.7.4 in __init__.py, pyproject.toml, and uv.lock 2026-06-12 20:51:52 -03:00
glomatico 69c2a8a063 Refactor GamdlApiResponseError to accept Any type for content and improve message formatting 2026-06-12 20:51:11 -03:00
Rafael Moraes fb143ad1b4 Merge pull request #315 from nirbhaykulkarni/fix/token-extraction-and-cover-timeout
Fix token extraction and cover art timeout
2026-06-12 20:46:11 -03:00
nirbhaykulkarni b66c06a9cb Fix token extraction and cover art timeout
- Search non-legacy index JS bundle for token (Apple moved it from index-legacy)
- Broaden JWT regex from eyJh to full 3-part JWT pattern (tokens now start with eyJ0)
- Add 30s timeout and follow_redirects to cover art fetch to avoid ConnectTimeout
2026-06-12 21:39:18 +05:30
glomatico a9e75384f0 Add method to switch m3u8 master URL to default and update playback handling 2026-06-05 22:07:31 -03:00
Rafael Moraes d88dbe5bb6 Bump version to 3.7.3 2026-05-28 17:28:47 -03:00
Rafael Moraes 8398d9c65f Handle missing playParams in metadata 2026-05-28 17:28:22 -03:00
Rafael Moraes c6bce4b2c1 Bump version to 3.7.2 2026-05-28 17:24:21 -03:00
Rafael Moraes f54ab12408 Guard playParams access to avoid KeyError 2026-05-28 17:23:35 -03:00
Rafael Moraes 817479d807 Use uncensored names and add sort fields 2026-05-28 17:20:44 -03:00
8 changed files with 56 additions and 25 deletions
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.7.1" __version__ = "3.7.4"
+2 -2
View File
@@ -93,7 +93,7 @@ class AppleMusicApi:
) )
index_js_uri_match = re.search( index_js_uri_match = re.search(
r"/(assets/index-legacy[~-][^/\"]+\.js)", r"/(assets/index[~-][^/\"]+\.js)",
home_page, home_page,
) )
if not index_js_uri_match: if not index_js_uri_match:
@@ -116,7 +116,7 @@ class AppleMusicApi:
status_code=response.status_code if response is not None else None, status_code=response.status_code if response is not None else None,
) )
token_match = re.search('(?=eyJh)(.*?)(?=")', index_js_page) token_match = re.search(r'"(eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+)"', index_js_page)
if not token_match: if not token_match:
raise GamdlApiResponseError("Error finding token in index.js page") raise GamdlApiResponseError("Error finding token in index.js page")
token = token_match.group(1) token = token_match.group(1)
+15 -3
View File
@@ -1,3 +1,6 @@
import json
from typing import Any
from ..utils import GamdlError from ..utils import GamdlError
@@ -9,7 +12,7 @@ class GamdlApiResponseError(GamdlApiError):
def __init__( def __init__(
self, self,
message: str, message: str,
content: str | None = None, content: Any | None = None,
status_code: int | None = None, status_code: int | None = None,
): ):
self.message = message self.message = message
@@ -19,7 +22,16 @@ class GamdlApiResponseError(GamdlApiError):
if status_code is not None: if status_code is not None:
message = f"{message} (Status code: {status_code})" message = f"{message} (Status code: {status_code})"
if content: if content is not None:
message += f": {content}" if isinstance(content, str):
content_text = content
else:
try:
content_text = json.dumps(content)
except TypeError:
content_text = str(content)
if content_text:
message += f": {content_text}"
super().__init__(message) super().__init__(message)
+2 -2
View File
@@ -205,8 +205,8 @@ class AppleMusicBaseInterface:
async def get_cover_bytes(self, cover_url: str) -> bytes | None: async def get_cover_bytes(self, cover_url: str) -> bytes | None:
log = logger.bind(action="get_cover_bytes", cover_url=cover_url) log = logger.bind(action="get_cover_bytes", cover_url=cover_url)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(cover_url) response = await client.get(cover_url, follow_redirects=True)
if response.status_code == 404: if response.status_code == 404:
log.debug("cover_not_found") log.debug("cover_not_found")
+5 -3
View File
@@ -127,7 +127,8 @@ class AppleMusicMusicVideoInterface:
genre_id=int(itunes_page_metadata["genres"][0]["genreId"]), genre_id=int(itunes_page_metadata["genres"][0]["genreId"]),
media_type=MediaType.MUSIC_VIDEO, media_type=MediaType.MUSIC_VIDEO,
storefront=self.base.itunes_api.storefront_id, storefront=self.base.itunes_api.storefront_id,
title=lookup_metadata[0]["trackCensoredName"], title=lookup_metadata[0]["trackName"],
title_sort=lookup_metadata[0]["trackCensoredName"],
title_id=int(metadata["id"]), title_id=int(metadata["id"]),
rating=rating, rating=rating,
) )
@@ -139,7 +140,8 @@ class AppleMusicMusicVideoInterface:
if not album: if not album:
return tags return tags
tags.album = lookup_metadata[1]["collectionCensoredName"] tags.album = lookup_metadata[1]["collectionName"]
tags.album_sort = lookup_metadata[1]["collectionCensoredName"]
tags.album_artist = lookup_metadata[1]["artistName"] tags.album_artist = lookup_metadata[1]["artistName"]
tags.album_id = int(itunes_page_metadata["collectionId"]) tags.album_id = int(itunes_page_metadata["collectionId"])
tags.disc = lookup_metadata[0]["discNumber"] tags.disc = lookup_metadata[0]["discNumber"]
@@ -441,7 +443,7 @@ class AppleMusicMusicVideoInterface:
) )
)["data"][0] )["data"][0]
if media.media_metadata["attributes"]["playParams"].get("isLibrary"): if media.media_metadata["attributes"].get("playParams", {}).get("isLibrary"):
catalog_metadata = self.base.get_catalog_metadata_from_library( catalog_metadata = self.base.get_catalog_metadata_from_library(
media.media_metadata media.media_metadata
) )
+29 -12
View File
@@ -191,18 +191,24 @@ class AppleMusicSongInterface:
return f"[{timestamp.strftime('%M:%S.%f')[:-4]}]{text}" return f"[{timestamp.strftime('%M:%S.%f')[:-4]}]{text}"
def _get_m3u8_from_playback(self, playback: dict) -> str | None: def _switch_m3u8_master_url_to_default(self, m3u8_master_url: str) -> str:
return playback["songList"][0].get("hls-playlist-url") return re.sub(
r"(P\d+)_[^/]+(\.m3u8)",
r"\1_default\2",
m3u8_master_url,
)
async def get_m3u8_master_url( def _get_m3u8_from_playback(self, playback: dict) -> str | None:
self, log = logger.bind(action="get_m3u8_master_url_from_playback")
playback: dict | None,
song_metadata: dict | None, m3u8_master_url = playback["songList"][0].get("hls-playlist-url")
) -> str | None:
if playback: if m3u8_master_url:
return self._get_m3u8_from_playback(playback) m3u8_master_url = self._switch_m3u8_master_url_to_default(m3u8_master_url)
else: log.debug("success", m3u8_master_url=m3u8_master_url)
return await self._get_m3u8_master_url_from_metadata(song_metadata) return m3u8_master_url
log.debug("no_m3u8_master_url")
async def _get_m3u8_master_url_from_metadata( async def _get_m3u8_master_url_from_metadata(
self, self,
@@ -227,6 +233,7 @@ class AppleMusicSongInterface:
enhanced = song_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls") enhanced = song_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls")
if enhanced: if enhanced:
enhanced = self._switch_m3u8_master_url_to_default(enhanced)
log.debug("success", m3u8_master_url=enhanced) log.debug("success", m3u8_master_url=enhanced)
return enhanced return enhanced
@@ -234,6 +241,16 @@ class AppleMusicSongInterface:
return None return None
async def get_m3u8_master_url(
self,
playback: dict | None,
song_metadata: dict | None,
) -> str | None:
if playback:
return self._get_m3u8_from_playback(playback)
else:
return await self._get_m3u8_master_url_from_metadata(song_metadata)
async def get_stream_info( async def get_stream_info(
self, self,
media_id: str, media_id: str,
@@ -506,7 +523,7 @@ class AppleMusicSongInterface:
) )
)["data"][0] )["data"][0]
if media.media_metadata["attributes"]["playParams"].get("isLibrary"): if media.media_metadata["attributes"].get("playParams", {}).get("isLibrary"):
catalog_metadata = self.base.get_catalog_metadata_from_library( catalog_metadata = self.base.get_catalog_metadata_from_library(
media.media_metadata media.media_metadata
) )
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "gamdl" name = "gamdl"
version = "3.7.1" version = "3.7.4"
description = "A command-line app for downloading Apple Music songs, music videos and post videos." description = "A command-line app for downloading Apple Music songs, music videos and post videos."
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
Generated
+1 -1
View File
@@ -223,7 +223,7 @@ wheels = [
[[package]] [[package]]
name = "gamdl" name = "gamdl"
version = "3.7.1" version = "3.7.4"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "async-lru" }, { name = "async-lru" },