Compare commits

...

7 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
8 changed files with 51 additions and 22 deletions
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.7.2"
__version__ = "3.7.4"
+2 -2
View File
@@ -93,7 +93,7 @@ class AppleMusicApi:
)
index_js_uri_match = re.search(
r"/(assets/index-legacy[~-][^/\"]+\.js)",
r"/(assets/index[~-][^/\"]+\.js)",
home_page,
)
if not index_js_uri_match:
@@ -116,7 +116,7 @@ class AppleMusicApi:
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:
raise GamdlApiResponseError("Error finding token in index.js page")
token = token_match.group(1)
+15 -3
View File
@@ -1,3 +1,6 @@
import json
from typing import Any
from ..utils import GamdlError
@@ -9,7 +12,7 @@ class GamdlApiResponseError(GamdlApiError):
def __init__(
self,
message: str,
content: str | None = None,
content: Any | None = None,
status_code: int | None = None,
):
self.message = message
@@ -19,7 +22,16 @@ class GamdlApiResponseError(GamdlApiError):
if status_code is not None:
message = f"{message} (Status code: {status_code})"
if content:
message += f": {content}"
if content is not None:
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)
+2 -2
View File
@@ -205,8 +205,8 @@ class AppleMusicBaseInterface:
async def get_cover_bytes(self, cover_url: str) -> bytes | None:
log = logger.bind(action="get_cover_bytes", cover_url=cover_url)
async with httpx.AsyncClient() as client:
response = await client.get(cover_url)
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(cover_url, follow_redirects=True)
if response.status_code == 404:
log.debug("cover_not_found")
+1 -1
View File
@@ -443,7 +443,7 @@ class AppleMusicMusicVideoInterface:
)
)["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(
media.media_metadata
)
+28 -11
View File
@@ -191,18 +191,24 @@ class AppleMusicSongInterface:
return f"[{timestamp.strftime('%M:%S.%f')[:-4]}]{text}"
def _get_m3u8_from_playback(self, playback: dict) -> str | None:
return playback["songList"][0].get("hls-playlist-url")
def _switch_m3u8_master_url_to_default(self, m3u8_master_url: str) -> str:
return re.sub(
r"(P\d+)_[^/]+(\.m3u8)",
r"\1_default\2",
m3u8_master_url,
)
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)
def _get_m3u8_from_playback(self, playback: dict) -> str | None:
log = logger.bind(action="get_m3u8_master_url_from_playback")
m3u8_master_url = playback["songList"][0].get("hls-playlist-url")
if m3u8_master_url:
m3u8_master_url = self._switch_m3u8_master_url_to_default(m3u8_master_url)
log.debug("success", m3u8_master_url=m3u8_master_url)
return m3u8_master_url
log.debug("no_m3u8_master_url")
async def _get_m3u8_master_url_from_metadata(
self,
@@ -227,6 +233,7 @@ class AppleMusicSongInterface:
enhanced = song_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls")
if enhanced:
enhanced = self._switch_m3u8_master_url_to_default(enhanced)
log.debug("success", m3u8_master_url=enhanced)
return enhanced
@@ -234,6 +241,16 @@ class AppleMusicSongInterface:
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(
self,
media_id: str,
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "gamdl"
version = "3.7.2"
version = "3.7.4"
description = "A command-line app for downloading Apple Music songs, music videos and post videos."
readme = "README.md"
license = "MIT"
Generated
+1 -1
View File
@@ -223,7 +223,7 @@ wheels = [
[[package]]
name = "gamdl"
version = "3.7.2"
version = "3.7.4"
source = { virtual = "." }
dependencies = [
{ name = "async-lru" },