from __future__ import annotations from typing import Any, Iterable, Mapping, Optional, Sequence from src.api_client.dsp import AppleMusicApiClient from src.data_info import PlaylistInfo, TrackInfo from src.enums import ServiceType from src.service_account.models import ServiceAccount from src.synchronizer.dsp.base import BaseSynchronizer from src.synchronizer.errors import AppleMusicSynchronizerError __all__ = ["AppleMusicSynchronizer"] class AppleMusicSynchronizer(BaseSynchronizer[AppleMusicApiClient]): service_type = ServiceType.apple_music api_client_cls = AppleMusicApiClient vendor_specific_sync_error_cls = AppleMusicSynchronizerError def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self._playlist_id_map: Mapping[str, str] | None = None def _get_internal_playlist_id(self, playlist_id: str, raise_error: bool = True) -> Optional[str]: if self._playlist_id_map is None: self._playlist_id_map = self.api_client.get_playlist_id_map() self._logger.info( f"Getting internal apple playlist id for '{playlist_id}' from map {self._playlist_id_map}" ) if playlist_id not in self._playlist_id_map: if raise_error: raise AppleMusicSynchronizerError(f"Unknown playlist id {playlist_id}") return None return self._playlist_id_map[playlist_id] def _get_tracks_from_dsp(self, tracks: Iterable[TrackInfo]) -> Mapping[TrackInfo, str]: storefront = self.api_client.get_storefront() # TODO: consider do searching by title if there is no ISRC for track # TODO: consider do searching by title if we can't find track by ISRC isrc_list = [track.isrc for track in tracks if track.isrc] isrc_to_track_id_map = self.api_client.get_track_id_by_isrc(isrc_list=isrc_list, storefront=storefront) return {track: isrc_to_track_id_map[track.isrc] for track in tracks if track.isrc in isrc_to_track_id_map} def _get_target_track_ids(self, playlist_id: str) -> Iterable[str]: internal_playlist_id = self._get_internal_playlist_id(playlist_id) return self.api_client.get_playlist_track_ids(internal_playlist_id, True) @staticmethod def skip_existing_tracks(source_tracks: Sequence[TrackInfo], target_track_ids: Iterable[str]): return [i for i in source_tracks if i.isrc not in target_track_ids] def _synchronize_tracks( self, playlist_id: str, source_track_ids: Sequence[str], target_track_ids: Sequence[str] ) -> tuple[int, int]: # Apple Music Api has no way to delete tracks internal_playlist_id = self._get_internal_playlist_id(playlist_id) self.api_client.insert_tracks(playlist_id=internal_playlist_id, track_ids=source_track_ids) return len(source_track_ids), 0 def get_playlist_info(self, playlist_id: str) -> PlaylistInfo: internal_playlist_id = self._get_internal_playlist_id(playlist_id) return self.api_client.get_playlist_info(internal_playlist_id) def _update_playlist_info(self, playlist_id: str, old_info: PlaylistInfo, new_info: Mapping[str, str | None]): # Apple Music Api has no way to update playlist info pass def check_target_playlist(self, service_account: ServiceAccount, playlist_id: str) -> bool: return bool(self._get_internal_playlist_id(playlist_id, raise_error=False))