11import urllib .parse
2- import requests , json
2+ import requests
3+ import json
34from bs4 import BeautifulSoup
45
6+
57def get_cover (url , width , height , format = "jpg" , crop_option = "" ):
68 """
79 Generate a full Apple Music artwork URL with proper width, height, format, and crop settings.
8-
9- Parameters
10- ----------
11- url : str
12- The original Apple Music artwork template URL containing `{w}`, `{h}`, `{f}`, `{c}`.
13- width : int or str
14- Target width of the image.
15- height : int or str
16- Target height of the image.
17- format : str, optional
18- Image format (jpg, png, etc.). Defaults to `"jpg"`.
19- crop_option : str, optional
20- Cropping mode used by Apple Music artwork URLs. Defaults to empty string.
21-
22- Returns
23- -------
24- str
25- Fully formatted artwork URL.
26-
27- Notes
28- -----
29- Apple Music uses dynamic artwork URLs where dimensions and format are embedded
30- in the URL as placeholders such as `{w}`, `{h}`, `{f}`, and `{c}`.
3110 """
32- new_url = url .replace ("{w}" , str (width ))
33- new_url = new_url .replace ("{h}" , str (height ))
34- new_url = new_url .replace ("{c}" , crop_option )
35- new_url = new_url .replace ("{f}" , format )
36- return new_url
11+ if not isinstance (url , str ):
12+ return url
13+
14+ try :
15+ new_url = (
16+ url .replace ("{w}" , str (width ))
17+ .replace ("{h}" , str (height ))
18+ .replace ("{c}" , crop_option )
19+ .replace ("{f}" , format )
20+ )
21+ return new_url
22+ except (TypeError , AttributeError ):
23+ return url
24+
3725
3826def convert_album_to_song_url (album_url ):
3927 """
4028 Convert an Apple Music album-track URL into a direct Apple Music song URL.
41-
42- Parameters
43- ----------
44- album_url : str
45- Full Apple Music album URL that contains a track ID via the query parameter `?i=...`.
46-
47- Returns
48- -------
49- str or None
50- Direct Apple Music song URL if `i` parameter exists.
51- Otherwise, returns `None`.
52-
53- Examples
54- --------
55- Input:
56- https://music.apple.com/us/album/song-name/12345?i=67890
57-
58- Output:
59- https://music.apple.com/us/song/song-name/67890
60-
61- Notes
62- -----
63- Apple Music album pages embed individual song IDs through the query parameter `i`,
64- which must be extracted and placed into a `/song/` URL.
6529 """
66- parsed = urllib .parse .urlparse (album_url )
67- query_params = urllib .parse .parse_qs (parsed .query )
68- song_id = query_params .get ('i' , [None ])[0 ]
30+ try :
31+ parsed = urllib .parse .urlparse (album_url )
32+ query_params = urllib .parse .parse_qs (parsed .query )
33+ song_id = query_params .get ("i" , [None ])[0 ]
6934
70- if not song_id :
71- return None
35+ if not song_id :
36+ return None
37+
38+ parts = parsed .path .split ("/" )
39+ if len (parts ) < 4 :
40+ return None
7241
73- parts = parsed .path .split ('/' )
74- country = parts [1 ]
75- title = parts [3 ]
42+ country = parts [1 ]
43+ title = parts [3 ]
44+
45+ return f"https://music.apple.com/{ country } /song/{ title } /{ song_id } "
46+
47+ except (IndexError , KeyError , TypeError , AttributeError , ValueError ):
48+ return None
7649
77- return f"https://music.apple.com/{ country } /song/{ title } /{ song_id } "
7850
7951def get_all_singles (url = "https://music.apple.com/us/artist/king-princess/1349968534" ):
8052 """
8153 Fetch all singles & EP URLs from an Apple Music artist page.
82-
83- Parameters
84- ----------
85- url : str, optional
86- Base artist page URL. Defaults to the sample King Princess artist link.
87-
88- Returns
89- -------
90- list[str]
91- A list of Apple Music URLs for all singles & EPs for the artist.
92-
93- Notes
94- -----
95- - Apple Music loads singles under the `/see-all?section=singles` endpoint.
96- - This function retrieves the serialized server data, parses the `items` section,
97- and extracts the correct song/EP URLs.
98- - Used internally by `artist_scrape()`.
9954 """
10055 result = []
101- url = url + "/see-all?section=singles"
102-
103- headers = {
104- "User-Agent" : "Mozilla/5.0"
105- }
10656
107- res = requests .get (url , headers = headers )
57+ full_url = f"{ url } /see-all?section=singles"
58+ headers = {"User-Agent" : "Mozilla/5.0" }
59+
60+ try :
61+ res = requests .get (full_url , headers = headers , timeout = 10 )
62+ res .raise_for_status ()
63+ except requests .RequestException :
64+ return result
65+
10866 soup = BeautifulSoup (res .text , "html.parser" )
109- items = soup .find ('script' , {'id' : 'serialized-server-data' })
110- our_json = json .loads (items .text )
111-
112- sections = our_json [0 ]['data' ]['sections' ][0 ]['items' ]
113-
114- for i in sections :
115- result .append ((i ['segue' ]['actionMetrics' ]['data' ][0 ]['fields' ]['actionUrl' ]))
116-
117- return result
67+ script_tag = soup .find ("script" , {"id" : "serialized-server-data" })
68+ if not script_tag :
69+ return result
70+
71+ try :
72+ data = json .loads (script_tag .text )
73+ sections = data [0 ]["data" ]["sections" ]
74+ if not sections :
75+ return result
76+
77+ items = sections [0 ].get ("items" , [])
78+ except (json .JSONDecodeError , KeyError , IndexError , TypeError ):
79+ return result
80+
81+ for item in items :
82+ try :
83+ action_url = item ["segue" ]["actionMetrics" ]["data" ][0 ]["fields" ]["actionUrl" ]
84+ result .append (action_url )
85+ except (KeyError , IndexError , TypeError ):
86+ continue
87+
88+ return result
0 commit comments