# pylint: disable=no-self-argument,bare-except from datetime import date, datetime, timezone from typing import Any, Dict, List, Optional, Union import orjson from pydantic import BaseModel as PydanticBaseModel # pylint: disable=no-name-in-module from pydantic import Field, root_validator, validator class BaseModel(PydanticBaseModel): class Config: json_dumps = orjson.dumps json_loads = orjson.loads class Entity(BaseModel): """ This class stores attributes which shared by most of objects in db""" dsp_id: int market_id: int updated_at: datetime is_stream_synced: bool = False class ArtistFollowers(BaseModel): artist_id: str followers: int class TrackAlbum(BaseModel): dsp_id: int market_id: int updated_at: datetime track_id: str album_id: str class AlbumArtist(BaseModel): dsp_id: int market_id: int updated_at: datetime album_id: str artist_id: str = Field(alias="id") class TrackArtist(BaseModel): dsp_id: int market_id: int updated_at: datetime track_id: str artist_id: str artist_number: int class SpotifyArtist(Entity): artist_id: str = Field(alias="id") artist_uri: str = Field(alias="uri") artist_name: str = Field(alias="name") genres: Optional[List[str]] url: Optional[str] artist_followers: Optional[ArtistFollowers] @root_validator(pre=True) def artist_followers_setter(cls, artist: Dict[str, Any]) -> Dict[str, Any]: if artist.get("followers", {}).get("total"): artist["artist_followers"] = { "artist_id": artist["id"], "followers": artist["followers"].get("total"), } return artist class SpotifyAlbum(Entity): album_id: str = Field(alias="id") album_name: str = Field(alias="name") release_date: Optional[str] release_date_precision: Optional[str] track_count: int = Field(alias="total_tracks") upc: Optional[str] image_path: Optional[str] artists: Optional[List[SpotifyArtist]] album_artists: Optional[List[AlbumArtist]] @root_validator(pre=True) def data_extractor(cls, album: Dict[str, Any]) -> Dict[str, Any]: album.update(album.get("external_ids", {})) if album.get("artists"): for artist in album["artists"]: artist["dsp_id"] = album["dsp_id"] artist["market_id"] = album["market_id"] artist["updated_at"] = album["updated_at"] artist["album_id"] = album["id"] album["album_artists"] = album["artists"] if album.get("images"): album["image_path"] = album["images"][0]["url"] return album class SpotifyTrack(Entity): track_id: str = Field(alias="id") track_name: Optional[str] = Field(alias="name") track_duration: int = Field(0, alias="duration_ms") isrc: Optional[str] release_date: Optional[str] url: Optional[str] popularity: Optional[int] artists: Optional[List[SpotifyArtist]] albums: Optional[List[SpotifyAlbum]] track_albums: Optional[List[TrackAlbum]] track_artists: Optional[List[TrackArtist]] @root_validator(pre=True) def track_preprocessor(cls, track: Dict[str, Any]) -> Dict[str, Any]: track.update(track.get("external_ids", {})) track["url"] = track.get("external_urls", {}).get("spotify") if track.get("album"): track["album"]["dsp_id"] = track["dsp_id"] track["album"]["market_id"] = track["market_id"] track["album"]["updated_at"] = track["updated_at"] track["album"]["track_id"] = track["id"] track["album"]["album_id"] = track["album"]["id"] track["albums"] = [track["album"]] track["track_albums"] = track["albums"] if track.get("artists"): index = 1 for artist in track["artists"]: artist["dsp_id"] = track["dsp_id"] artist["market_id"] = track["market_id"] artist["updated_at"] = track["updated_at"] artist["track_id"] = track["id"] artist["artist_id"] = artist["id"] artist["artist_number"] = index index += 1 track["track_artists"] = track["artists"] return track class TrackPosition(BaseModel): position: int track_id: str = Field(alias="id") added_at: Optional[str] class PlaylistMetadata(BaseModel): playlist_id: str dsp_id: int market_id: int track_positions: List[TrackPosition] class PlaylistOwnerCategory(BaseModel): id: int = Field(alias="bu_category_id") name: str = Field(alias="bc_name") created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class PlaylistOwner(BaseModel): dsp_id: int username: Optional[str] = Field(alias="bu_username") display_name: Optional[str] = Field(alias="bu_display_name") category_id: Optional[int] = Field(alias="bu_category_id") market_id: Optional[Union[int, str]] = Field(alias="bu_country_code") created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class SpotifyPlaylist(Entity): playlist_id: str = Field(alias="id") playlist_name: Optional[str] = Field(alias="name") uri: str playlist_owner_username: Optional[str] = Field(alias="bu_username") type: Optional[str] = Field(alias="playlist_type") is_public: bool = Field(alias="public") is_personalized: bool image_path: Optional[str] followers: Optional[int] is_removed: Optional[bool] is_ignored: Optional[bool] description: Optional[str] dsp_last_update_datetime: Optional[datetime] last_sync_datetime: Optional[datetime] rank: Optional[int] save_tracklist: bool save_tracklist_history: bool collect_historical_data: bool = Field(alias="save_tracklist_history") tracks: Optional[List[SpotifyTrack]] playlist_metadata: Optional[PlaylistMetadata] playlist_owner_category: Optional[PlaylistOwnerCategory] playlist_owner: Optional[PlaylistOwner] @validator("playlist_name") def replace_null_char(cls, playlist_name: str) -> str: return playlist_name.replace("\x00", "") @root_validator(pre=True) def playlist_preprocessor(cls, playlist: Dict[str, Any]) -> Dict[str, Any]: if playlist.get("images"): playlist["image_path"] = playlist["images"][0]["url"] if playlist.get("followers"): playlist["followers"] = playlist.get("followers", {}).get("total") if playlist.get("bu_username"): playlist["playlist_owner"] = playlist.copy() if playlist.get("bc_name"): playlist["playlist_owner_category"] = playlist.copy() if not playlist.get("tracks"): return playlist tracks = [] index = 0 try: total_tracks = len(playlist["tracks"]["items"]) except: total_tracks = 0 try: for track in playlist["tracks"]["items"]: if not track: index += 1 continue if isinstance(track.get("track"), dict): track.update(track.get("track")) if not track.get("id"): index += 1 continue if not track.get("name"): index += 1 continue try: track["market_id"] = playlist["market_id"] track["dsp_id"] = playlist["dsp_id"] track["updated_at"] = playlist["updated_at"] track["position"] = index track["total_tracks"] = total_tracks track["playlist_id"] = playlist["id"] tracks.append(track) index += 1 except: pass except: pass playlist["tracks"] = tracks if playlist["save_tracklist"]: metadata = { "playlist_id": playlist["id"], "dsp_id": playlist["dsp_id"], "market_id": playlist["market_id"], "track_positions": playlist["tracks"], } playlist["playlist_metadata"] = metadata return playlist class AppleMusicEntity(Entity): image_path: Optional[str] = Field(alias="artwork") @root_validator(pre=True) def attributes_extract(cls, entity): entity.update(entity.pop("attributes", {})) return entity @validator("image_path", pre=True) def image_path_constructor(cls, artwork: Dict[str, Any]) -> Union[str, None]: try: url: str = artwork["url"] width: str | int = artwork["width"] height: str | int = artwork["height"] image_path = url.replace(r"{w}", str(width)).replace(r"{h}", str(height)) return image_path except (KeyError, TypeError): return None class AppleMusicArtist(AppleMusicEntity): artist_id: str = Field(alias="id") artist_name: Optional[str] = Field("", alias="name") artist_uri: Optional[str] = Field(alias="href") is_stream_synced: bool = False url: Optional[str] @root_validator(pre=True) def data_extractor(cls, artist: Dict[str, Any]) -> Dict[str, Any]: if artist.get("data"): artist.update(artist.pop("data")[0]) artist.update(artist.pop("attributes", {})) return artist class AppleMusicAlbum(AppleMusicEntity): album_id: str = Field(alias="id") album_name: Optional[str] = Field("", alias="name") release_date: Optional[str] = Field(alias="releaseDate") track_count: Optional[int] = Field(alias="trackCount") is_mastered_for_itunes: Optional[bool] = Field(False, alias="isMasteredForItunes") is_complete: Optional[bool] = Field(False, alias="isComplete") copyright: Optional[str] = Field(alias="copyright") lbl: Optional[str] = Field(alias="recordLabel") is_stream_synced: bool = False upc: Optional[str] artists: Optional[List[AppleMusicArtist]] album_artists: Optional[List[AlbumArtist]] @root_validator(pre=True) def data_extractor(cls, album: Dict[str, Any]) -> Dict[str, Any]: if album.get("data"): album.update(album.pop("data")[0]) album.update(album.pop("attributes", {})) album.update(album.pop("relationships", {})) if album.get("artists"): for artist in album["artists"]["data"]: artist["dsp_id"] = album["dsp_id"] artist["market_id"] = album["market_id"] artist["updated_at"] = album["updated_at"] artist["album_id"] = album["id"] album["artists"] = album["artists"]["data"] album["album_artists"] = album["artists"] return album class AppleMusicTrack(AppleMusicEntity): track_id: str = Field(alias="id") track_name: Optional[str] = Field(alias="name") track_duration: Optional[int] = Field(0, alias="durationInMillis") isrc: Optional[str] release_date: Optional[date] = Field(alias="releaseDate") url: Optional[str] albums: Optional[List[AppleMusicAlbum]] artists: Optional[List[AppleMusicArtist]] track_albums: Optional[List[TrackAlbum]] track_artists: Optional[List[TrackArtist]] @root_validator(pre=True) def data_extractor(cls, track: Dict[str, Any]) -> Dict[str, Any]: if track.get("data"): track.update(track.pop("data")[0]) track.update(track.pop("attributes", {})) track.update(track.pop("relationships", {})) if track.get("albums"): track["albums"] = track["albums"]["data"] for album in track["albums"]: album["dsp_id"] = track["dsp_id"] album["market_id"] = track["market_id"] album["updated_at"] = track["updated_at"] album["track_id"] = track["id"] album["album_id"] = album["id"] track["track_albums"] = track["albums"] if track.get("artists"): index = 1 track["artists"] = track["artists"]["data"] for artist in track["artists"]: artist["dsp_id"] = track["dsp_id"] artist["market_id"] = track["market_id"] artist["updated_at"] = track["updated_at"] artist["artist_id"] = artist["id"] artist["track_id"] = track["id"] artist["artist_number"] = index index += 1 track["track_artists"] = track["artists"] return track class AppleMusicPlaylist(AppleMusicEntity): playlist_id: str = Field(alias="id") playlist_name: Optional[str] = Field(alias="name") type: Optional[str] = Field(alias="playlist_type") uri: str = Field(alias="href") playlist_owner_username: Optional[str] = Field(alias="bu_username") followers: Optional[int] is_removed: Optional[bool] is_ignored: Optional[bool] description: Optional[str] dsp_last_update_datetime: Optional[datetime] = Field(alias="lastModifiedDate") last_sync_datetime: Optional[datetime] rank: Optional[int] collect_historical_data: bool = Field(alias="save_tracklist_history") tracks: List[AppleMusicTrack] playlist_metadata: Optional[PlaylistMetadata] playlist_owner_category: Optional[PlaylistOwnerCategory] playlist_owner: Optional[PlaylistOwner] @validator("playlist_name") def replace_null_char(cls, playlist_name: str) -> str: return playlist_name.replace("\x00", "") @root_validator(pre=True) def playlist_preprocessor(cls, playlist: Dict[str, Any]) -> Dict[str, Any]: """ Extracts nested fields, adds positions to tracks, filters tracks without ids enriches tracks with dsp_id, market_id and updated_at fields creates playlist_metadata object """ # playlist.update(playlist.pop('data')[0]) # playlist.update(playlist.pop('attributes')) playlist.update(playlist.pop("relationships")) playlist["tracks"] = playlist["tracks"]["data"] tracks = [] index = 0 try: total_tracks = len(playlist["tracks"]) except: # pylint: disable=bare-except total_tracks = 0 try: for track in playlist["tracks"]: try: if not track["id"]: continue track["market_id"] = playlist["market_id"] track["dsp_id"] = playlist["dsp_id"] track["updated_at"] = playlist["updated_at"] track["position"] = index track["playlist_id"] = playlist["id"] track["total_tracks"] = total_tracks tracks.append(track) index += 1 except: pass except: pass playlist["tracks"] = tracks if playlist["save_tracklist"]: metadata = { "playlist_id": playlist["id"], "dsp_id": playlist["dsp_id"], "market_id": playlist["market_id"], "track_positions": playlist["tracks"], } playlist["playlist_metadata"] = metadata if playlist.get("description"): playlist["description"] = playlist.get("description", {}).get("standard") if playlist.get("bu_username"): playlist["playlist_owner"] = playlist.copy() if playlist.get("bc_name"): playlist["playlist_owner_category"] = playlist.copy() return playlist class PlaylistFollowers(BaseModel): playlist_id: str = Field(alias="id") followers: int dsp_id: int market_id: int @validator("followers", pre=True) def followers_converter(cls, followers): if followers: return followers.get("total") or 0 return 0