107107
108108CONF_MUSIC_APP_TOKEN = "music_app_token"
109109CONF_MUSIC_USER_TOKEN = "music_user_token"
110+ CONF_MUSIC_USER_MANUAL_TOKEN = "music_user_manual_token"
110111CONF_MUSIC_USER_TOKEN_TIMESTAMP = "music_user_token_timestamp"
111112CACHE_CATEGORY_DECRYPT_KEY = 1
112113
@@ -236,7 +237,7 @@ async def serve_mk_glue(request: web.Request) -> web.Response:
236237 key = CONF_MUSIC_USER_TOKEN ,
237238 type = ConfigEntryType .SECURE_STRING ,
238239 label = "Music User Token" ,
239- required = True ,
240+ required = False ,
240241 action = "CONF_ACTION_AUTH" ,
241242 description = "Authenticate with Apple Music to retrieve a valid music user token." ,
242243 action_label = "Authenticate with Apple Music" ,
@@ -250,6 +251,19 @@ async def serve_mk_glue(request: web.Request) -> web.Response:
250251 )
251252 else None ,
252253 ),
254+ ConfigEntry (
255+ key = CONF_MUSIC_USER_MANUAL_TOKEN ,
256+ type = ConfigEntryType .SECURE_STRING ,
257+ label = "Manual Music User Token" ,
258+ required = False ,
259+ category = "advanced" ,
260+ description = (
261+ "Authenticate with a manual Music User Token in case the Authentication flow"
262+ " is unsupported (e.g. when using child accounts)."
263+ ),
264+ help_link = "https://www.music-assistant.io/music-providers/apple-music/" ,
265+ value = values .get (CONF_MUSIC_USER_MANUAL_TOKEN ),
266+ ),
253267 ConfigEntry (
254268 key = CONF_MUSIC_USER_TOKEN_TIMESTAMP ,
255269 type = ConfigEntryType .INTEGER ,
@@ -278,7 +292,9 @@ class AppleMusicProvider(MusicProvider):
278292
279293 async def handle_async_init (self ) -> None :
280294 """Handle async initialization of the provider."""
281- self ._music_user_token = self .config .get_value (CONF_MUSIC_USER_TOKEN )
295+ self ._music_user_token = self .config .get_value (
296+ CONF_MUSIC_USER_MANUAL_TOKEN
297+ ) or self .config .get_value (CONF_MUSIC_USER_TOKEN )
282298 self ._music_app_token = self .config .get_value (CONF_MUSIC_APP_TOKEN )
283299 self ._storefront = await self ._get_user_storefront ()
284300 # create random session id to use for decryption keys
@@ -349,11 +365,29 @@ async def get_library_artists(self) -> AsyncGenerator[Artist, None]:
349365 async def get_library_albums (self ) -> AsyncGenerator [Album , None ]:
350366 """Retrieve library albums from the provider."""
351367 endpoint = "me/library/albums"
352- for item in await self ._get_all_items (
368+ album_items = await self ._get_all_items (
353369 endpoint , include = "catalog,artists" , extend = "editorialNotes"
354- ):
370+ )
371+ album_catalog_item_ids = [
372+ item ["id" ]
373+ for item in album_items
374+ if item and item ["id" ] and not self .is_library_id (item ["id" ])
375+ ]
376+ album_library_item_ids = [
377+ item ["id" ]
378+ for item in album_items
379+ if item and item ["id" ] and self .is_library_id (item ["id" ])
380+ ]
381+ rating_catalog_response = await self ._get_ratings (album_catalog_item_ids , MediaType .ALBUM )
382+ rating_library_response = await self ._get_ratings (album_library_item_ids , MediaType .ALBUM )
383+ for item in album_items :
355384 if item and item ["id" ]:
356- album = self ._parse_album (item )
385+ is_favourite = (
386+ rating_catalog_response .get (item ["id" ])
387+ if not self .is_library_id (item ["id" ])
388+ else rating_library_response .get (item ["id" ])
389+ )
390+ album = self ._parse_album (item , is_favourite )
357391 if album :
358392 yield album
359393
@@ -384,18 +418,33 @@ async def get_library_tracks(self) -> AsyncGenerator[Track, None]:
384418 track = self ._parse_track (item , is_favourite )
385419 yield track
386420 # Yield library-only tracks using their library metadata
421+ library_ids = [item ["id" ] for item in library_only_tracks if item and item ["id" ]]
422+ library_rating_response = await self ._get_ratings (library_ids , MediaType .TRACK )
387423 for item in library_only_tracks :
388- yield self ._parse_track (item )
424+ is_favourite = library_rating_response .get (item ["id" ])
425+ yield self ._parse_track (item , is_favourite )
389426
390427 async def get_library_playlists (self ) -> AsyncGenerator [Playlist , None ]:
391428 """Retrieve playlists from the provider."""
392429 endpoint = "me/library/playlists"
393- for item in await self ._get_all_items (endpoint ):
430+ playlist_items = await self ._get_all_items (endpoint )
431+ playlist_library_item_ids = [
432+ item ["id" ]
433+ for item in playlist_items
434+ if item and item ["id" ] and self .is_library_id (item ["id" ])
435+ ]
436+ rating_library_response = await self ._get_ratings (
437+ playlist_library_item_ids , MediaType .PLAYLIST
438+ )
439+ for item in playlist_items :
440+ is_favourite = rating_library_response .get (item ["id" ])
394441 # Prefer catalog information over library information in case of public playlists
395442 if item ["attributes" ]["hasCatalog" ]:
396- yield await self .get_playlist (item ["attributes" ]["playParams" ]["globalId" ])
443+ yield await self .get_playlist (
444+ item ["attributes" ]["playParams" ]["globalId" ], is_favourite
445+ )
397446 elif item and item ["id" ]:
398- yield self ._parse_playlist (item )
447+ yield self ._parse_playlist (item , is_favourite )
399448
400449 @use_cache ()
401450 async def get_artist (self , prov_artist_id ) -> Artist :
@@ -409,7 +458,9 @@ async def get_album(self, prov_album_id) -> Album:
409458 """Get full album details by id."""
410459 endpoint = f"catalog/{ self ._storefront } /albums/{ prov_album_id } "
411460 response = await self ._get_data (endpoint , include = "artists" )
412- return self ._parse_album (response ["data" ][0 ])
461+ rating_response = await self ._get_ratings ([prov_album_id ], MediaType .ALBUM )
462+ is_favourite = rating_response .get (prov_album_id )
463+ return self ._parse_album (response ["data" ][0 ], is_favourite )
413464
414465 @use_cache ()
415466 async def get_track (self , prov_track_id ) -> Track :
@@ -421,15 +472,15 @@ async def get_track(self, prov_track_id) -> Track:
421472 return self ._parse_track (response ["data" ][0 ], is_favourite )
422473
423474 @use_cache ()
424- async def get_playlist (self , prov_playlist_id ) -> Playlist :
475+ async def get_playlist (self , prov_playlist_id , is_favourite : bool = False ) -> Playlist :
425476 """Get full playlist details by id."""
426- if self ._is_catalog_id (prov_playlist_id ):
477+ if not self .is_library_id (prov_playlist_id ):
427478 endpoint = f"catalog/{ self ._storefront } /playlists/{ prov_playlist_id } "
428479 else :
429480 endpoint = f"me/library/playlists/{ prov_playlist_id } "
430481 endpoint = f"catalog/{ self ._storefront } /playlists/{ prov_playlist_id } "
431482 response = await self ._get_data (endpoint )
432- return self ._parse_playlist (response ["data" ][0 ])
483+ return self ._parse_playlist (response ["data" ][0 ], is_favourite )
433484
434485 @use_cache ()
435486 async def get_album_tracks (self , prov_album_id ) -> list [Track ]:
@@ -464,9 +515,12 @@ async def get_playlist_tracks(self, prov_playlist_id, page: int = 0) -> list[Tra
464515 )
465516 if not response or "data" not in response :
466517 return result
518+ playlist_track_ids = [track ["id" ] for track in response ["data" ] if track and track ["id" ]]
519+ rating_response = await self ._get_ratings (playlist_track_ids , MediaType .TRACK )
467520 for index , track in enumerate (response ["data" ]):
468521 if track and track ["id" ]:
469- parsed_track = self ._parse_track (track )
522+ is_favourite = rating_response .get (track ["id" ])
523+ parsed_track = self ._parse_track (track , is_favourite )
470524 parsed_track .position = offset + index + 1
471525 result .append (parsed_track )
472526 return result
@@ -481,7 +535,17 @@ async def get_artist_albums(self, prov_artist_id) -> list[Album]:
481535 # Some artists do not have albums, return empty list
482536 self .logger .info ("No albums found for artist %s" , prov_artist_id )
483537 return []
484- return [self ._parse_album (album ) for album in response if album ["id" ]]
538+ album_ids = [album ["id" ] for album in response if album ["id" ]]
539+ rating_response = await self ._get_ratings (album_ids , MediaType .ALBUM )
540+ albums = []
541+ for album in response :
542+ if not album ["id" ]:
543+ continue
544+ is_favourite = rating_response .get (album ["id" ])
545+ parsed_album = self ._parse_album (album , is_favourite )
546+ if parsed_album :
547+ albums .append (parsed_album )
548+ return albums
485549
486550 @use_cache (3600 * 24 * 7 ) # cache for 7 days
487551 async def get_artist_toptracks (self , prov_artist_id ) -> list [Track ]:
@@ -493,7 +557,15 @@ async def get_artist_toptracks(self, prov_artist_id) -> list[Track]:
493557 # Some artists do not have top tracks, return empty list
494558 self .logger .info ("No top tracks found for artist %s" , prov_artist_id )
495559 return []
496- return [self ._parse_track (track ) for track in response ["data" ] if track ["id" ]]
560+ track_ids = [track ["id" ] for track in response ["data" ] if track ["id" ]]
561+ rating_response = await self ._get_ratings (track_ids , MediaType .TRACK )
562+ tracks = []
563+ for track in response ["data" ]:
564+ if not track ["id" ]:
565+ continue
566+ is_favourite = rating_response .get (track ["id" ])
567+ tracks .append (self ._parse_track (track , is_favourite ))
568+ return tracks
497569
498570 async def library_add (self , item : MediaItemType ) -> None :
499571 """Add item to library."""
@@ -548,9 +620,12 @@ async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]:
548620 response = await self ._post_data (endpoint , include = "artists" )
549621 if not response or "data" not in response :
550622 break
623+ track_ids = [track ["id" ] for track in response ["data" ] if track and track ["id" ]]
624+ rating_response = await self ._get_ratings (track_ids , MediaType .TRACK )
551625 for track in response ["data" ]:
552626 if track and track ["id" ]:
553- found_tracks .append (self ._parse_track (track ))
627+ is_favourite = rating_response .get (track ["id" ])
628+ found_tracks .append (self ._parse_track (track , is_favourite ))
554629 return found_tracks
555630
556631 async def get_stream_details (self , item_id : str , media_type : MediaType ) -> StreamDetails :
@@ -655,7 +730,9 @@ def _parse_artist(self, artist_obj: dict[str, Any]) -> Artist:
655730 artist .metadata .description = notes .get ("standard" ) or notes .get ("short" )
656731 return artist
657732
658- def _parse_album (self , album_obj : dict ) -> Album | ItemMapping | None :
733+ def _parse_album (
734+ self , album_obj : dict , is_favourite : bool | None = None
735+ ) -> Album | ItemMapping | None :
659736 """Parse album object to generic layout."""
660737 relationships = album_obj .get ("relationships" , {})
661738 response_type = album_obj .get ("type" )
@@ -747,7 +824,7 @@ def _parse_album(self, album_obj: dict) -> Album | ItemMapping | None:
747824 inferred_type = infer_album_type (album .name , "" )
748825 if inferred_type in (AlbumType .SOUNDTRACK , AlbumType .LIVE ):
749826 album .album_type = inferred_type
750-
827+ album . favorite = is_favourite or False
751828 return album
752829
753830 def _parse_track (
@@ -827,7 +904,9 @@ def _parse_track(
827904 track .favorite = is_favourite or False
828905 return track
829906
830- def _parse_playlist (self , playlist_obj : dict [str , Any ]) -> Playlist :
907+ def _parse_playlist (
908+ self , playlist_obj : dict [str , Any ], is_favourite : bool | None = None
909+ ) -> Playlist :
831910 """Parse Apple Music playlist object to generic layout."""
832911 attributes = playlist_obj ["attributes" ]
833912 playlist_id = attributes ["playParams" ].get ("globalId" ) or playlist_obj ["id" ]
@@ -861,6 +940,7 @@ def _parse_playlist(self, playlist_obj: dict[str, Any]) -> Playlist:
861940 )
862941 if description := attributes .get ("description" ):
863942 playlist .metadata .description = description .get ("standard" )
943+ playlist .favorite = is_favourite or False
864944 return playlist
865945
866946 async def _get_all_items (self , endpoint , key = "data" , ** kwargs ) -> list [dict ]:
@@ -994,7 +1074,10 @@ async def _get_ratings(self, item_ids: list[str], media_type: MediaType) -> dict
9941074 raise NotImplementedError (
9951075 "Ratings are not available for artist in the Apple Music API."
9961076 )
997- endpoint = self ._translate_media_type_to_apple_type (media_type )
1077+ if len (item_ids ) == 0 :
1078+ return {}
1079+ apple_type = self ._translate_media_type_to_apple_type (media_type )
1080+ endpoint = apple_type if not self .is_library_id (item_ids [0 ]) else f"library-{ apple_type } "
9981081 # Apple Music limits to 200 ids per request
9991082 max_ids_per_request = 200
10001083 results = {}
0 commit comments