from marshmallow import EXCLUDE, fields, post_dump, post_load, pre_dump, pre_load from marshmallow.schema import Schema from platypush.plugins.media import PlayerState from platypush.schemas import DateTime class MopidyTrackSchema(Schema): """ Mopidy track schema. """ uri = fields.String(required=True, metadata={"description": "Track URI"}) file = fields.String( metadata={"description": "Track URI, for MPD compatibility purposes"} ) artist = fields.String(missing=None, metadata={"description": "Artist name"}) title = fields.String(missing=None, metadata={"description": "Track title"}) album = fields.String(missing=None, metadata={"description": "Album name"}) artist_uri = fields.String( missing=None, metadata={"description": "Artist URI (if available)"} ) album_uri = fields.String( missing=None, metadata={"description": "Album URI (if available)"} ) time = fields.Float( missing=None, metadata={"description": "Track length (in seconds)"} ) playlist_pos = fields.Integer( missing=None, metadata={"description": "Track position in the tracklist/playlist"}, ) track_id = fields.Integer( missing=None, metadata={"description": "Track ID in the current tracklist"} ) track_no = fields.Integer( missing=None, metadata={"description": "Track number in the album"} ) date = fields.String(missing=None, metadata={"description": "Track release date"}) genre = fields.String(missing=None, metadata={"description": "Track genre"}) type = fields.Constant("track", metadata={"description": "Item type"}) @pre_load def parse(self, track: dict, **_): from platypush.plugins.music.mopidy import EmptyTrackException uri = (track or {}).get("uri", (track or {}).get("track", {}).get("uri")) if not uri: raise EmptyTrackException("Empty track") tlid = track.get("tlid") playlist_pos = track.get("playlist_pos") if track.get("track"): track = track.get("track", {}) length = track.get("length", track.get("time", track.get("duration"))) return { "uri": uri, "artist": next( iter(item.get("name") for item in track.get("artists", [])), None, ), "title": track.get("name"), "album": track.get("album", {}).get("name"), "artist_uri": next( iter(item.get("uri") for item in track.get("artists", [])), None ), "album_uri": track.get("album", {}).get("uri"), "time": length / 1000 if length is not None else None, "playlist_pos": ( track.get("playlist_pos") if playlist_pos is None else playlist_pos ), "date": track.get("date", track.get("album", {}).get("date")), "track_id": tlid, "track_no": track.get("track_no"), "genre": track.get("genre"), } @post_dump def to_dict(self, track: dict, **_): """ Fill/move missing fields in the dictionary. """ return { "file": track["uri"], **track, } class MopidyStatusSchema(Schema): """ Mopidy status schema. """ state = fields.Enum( PlayerState, required=True, metadata={"description": "Player state"}, ) volume = fields.Float(metadata={"description": "Player volume (0-100)"}) consume = fields.Boolean(metadata={"description": "Consume mode"}) random = fields.Boolean(metadata={"description": "Random mode"}) repeat = fields.Boolean(metadata={"description": "Repeat mode"}) single = fields.Boolean(metadata={"description": "Single mode"}) mute = fields.Boolean(metadata={"description": "Mute mode"}) time = fields.Float(metadata={"description": "Current time (in seconds)"}) playing_pos = fields.Integer( metadata={"description": "Index of the currently playing track"} ) track = fields.Nested( MopidyTrackSchema, missing=None, metadata={"description": "Current track"} ) @post_dump def post_dump(self, data: dict, **_): """ Post-dump hook. """ state = data.get("state") if state: data["state"] = getattr(PlayerState, state).value return data class MopidyPlaylistSchema(Schema): """ Mopidy playlist schema. """ # pylint: disable=too-few-public-methods class Meta: # type: ignore """ Mopidy playlist schema metadata. """ unknown = EXCLUDE uri = fields.String(required=True, metadata={"description": "Playlist URI"}) name = fields.String(required=True, metadata={"description": "Playlist name"}) last_modified = DateTime(metadata={"description": "Last modified timestamp"}) tracks = fields.List( fields.Nested(MopidyTrackSchema), missing=None, metadata={"description": "Playlist tracks"}, ) type = fields.Constant("playlist", metadata={"description": "Item type"}) @pre_dump def pre_dump(self, playlist, **_): """ Pre-dump hook. """ last_modified = ( playlist.last_modified if hasattr(playlist, "last_modified") else playlist.get("last_modified") ) if last_modified: last_modified /= 1000 if hasattr(playlist, "last_modified"): playlist.last_modified = last_modified else: playlist["last_modified"] = last_modified return playlist class MopidyArtistSchema(Schema): """ Mopidy artist schema. """ uri = fields.String(required=True, metadata={"description": "Artist URI"}) file = fields.String( metadata={"description": "Artist URI, for MPD compatibility purposes"} ) name = fields.String(missing=None, metadata={"description": "Artist name"}) artist = fields.String( missing=None, metadata={"description": "Same as name - for MPD compatibility purposes"}, ) type = fields.Constant("artist", metadata={"description": "Item type"}) @post_dump def to_dict(self, artist: dict, **_): """ Fill/move missing fields in the dictionary. """ return { "file": artist["uri"], "artist": artist["name"], **artist, } class MopidyAlbumSchema(Schema): """ Mopidy album schema. """ uri = fields.String(required=True, metadata={"description": "Album URI"}) file = fields.String( metadata={"description": "Artist URI, for MPD compatibility purposes"} ) artist = fields.String(missing=None, metadata={"description": "Artist name"}) album = fields.String( missing=None, metadata={"description": "Same as name - for MPD compatibility purposes"}, ) name = fields.String(missing=None, metadata={"description": "Album name"}) artist_uri = fields.String(missing=None, metadata={"description": "Artist URI"}) date = fields.String(missing=None, metadata={"description": "Album release date"}) genre = fields.String(missing=None, metadata={"description": "Album genre"}) def parse(self, data: dict, **_): assert data.get("uri"), "Album URI is required" return { "uri": data["uri"], "artist": data.get("artist") or next( iter(item.get("name") for item in data.get("artists", [])), None, ), "name": data.get("name"), "artist_uri": data.get("artist_uri") or next(iter(item.get("uri") for item in data.get("artists", [])), None), "album_uri": data.get("album_uri") or data.get("album", {}).get("uri"), "date": data.get("date", data.get("album", {}).get("date")), "genre": data.get("genre"), } @pre_load def pre_load(self, album: dict, **_): """ Pre-load hook. """ return self.parse(album) @pre_dump def pre_dump(self, album: dict, **_): """ Pre-dump hook. """ return self.parse(album) @post_dump def to_dict(self, album: dict, **_): """ Fill/move missing fields in the dictionary. """ return { "file": album["uri"], "album": album["name"], **album, } class MopidyDirectorySchema(Schema): """ Mopidy directory schema. """ uri = fields.String(required=True, metadata={"description": "Directory URI"}) name = fields.String(required=True, metadata={"description": "Directory name"}) type = fields.Constant("directory", metadata={"description": "Item type"}) class MopidyFilterSchema(Schema): """ Mopidy filter schema. """ uris = fields.List(fields.String, metadata={"description": "Filter by URIs"}) artist = fields.List(fields.String, metadata={"description": "Artist name(s)"}) album = fields.List(fields.String, metadata={"description": "Album name(s)"}) title = fields.List(fields.String, metadata={"description": "Track title(s)"}) albumartist = fields.List( fields.String, metadata={"description": "Album artist name(s)"} ) date = fields.List(fields.String, metadata={"description": "Track release date(s)"}) genre = fields.List(fields.String, metadata={"description": "Genre(s)"}) comment = fields.List(fields.String, metadata={"description": "Comment(s)"}) disc_no = fields.List(fields.Integer, metadata={"description": "Disc number(s)"}) musicbrainz_artistid = fields.List( fields.String, metadata={"description": "MusicBrainz artist ID(s)"} ) musicbrainz_albumid = fields.List( fields.String, metadata={"description": "MusicBrainz album ID(s)"} ) musicbrainz_trackid = fields.List( fields.String, metadata={"description": "MusicBrainz album artist ID(s)"} ) any = fields.List( fields.String, metadata={"description": "Generic search string(s)"} ) @pre_load def pre_load(self, data: dict, **_): """ Pre-load hook. """ for field_name, field in self.fields.items(): value = data.get(field_name) # Back-compatibtility with MPD's single-value filters if ( value is not None and isinstance(field, fields.List) and isinstance(value, str) ): data[field_name] = [value] return data @post_load def post_load(self, data: dict, **_): """ Post-load hook. """ title = data.pop("title", None) if title: data["track_name"] = title return data