Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various small bugfixes and improvements #807

Merged
merged 8 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 27 additions & 88 deletions music_assistant/common/models/media_items.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"""Models and helpers for media items."""
from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass, field, fields
from time import time
from typing import Any

from mashumaro import DataClassDictMixin

from music_assistant.common.helpers.json import json_dumps, json_loads
from music_assistant.common.helpers.uri import create_uri
from music_assistant.common.helpers.util import create_sort_name, merge_lists
from music_assistant.common.models.enums import (
Expand All @@ -21,8 +19,6 @@

MetadataTypes = int | bool | str | list[str]

JSON_KEYS = ("artists", "metadata", "provider_mappings")


@dataclass
class AudioFormat(DataClassDictMixin):
Expand Down Expand Up @@ -84,11 +80,16 @@ def quality(self) -> int:

def __hash__(self) -> int:
"""Return custom hash."""
return hash((self.provider_instance, self.item_id))
return hash((self.provider_instance, self.item_id.lower()))

def __eq__(self, other: ProviderMapping) -> bool:
"""Check equality of two items."""
return self.provider_instance == other.provider_instance and self.item_id == other.item_id
if not other:
return False
return (
self.provider_instance == other.provider_instance
and self.item_id.lower() == other.item_id.lower()
)


@dataclass(frozen=True)
Expand Down Expand Up @@ -198,16 +199,19 @@ def update(
return self


@dataclass
@dataclass(kw_only=True)
class MediaItem(DataClassDictMixin):
"""Base representation of a media item."""

media_type: MediaType
item_id: str
provider: str # provider instance id or provider domain
name: str
provider_mappings: set[ProviderMapping] = field(default_factory=set)
metadata: MediaItemMetadata
provider_mappings: set[ProviderMapping]

# optional fields below
# provider_mappings: set[ProviderMapping] = field(default_factory=set)
metadata: MediaItemMetadata = field(default_factory=MediaItemMetadata)
favorite: bool = False
media_type: MediaType = MediaType.UNKNOWN
Expand All @@ -225,44 +229,6 @@ def __post_init__(self):
if not self.sort_name:
self.sort_name = create_sort_name(self.name)

@classmethod
def from_db_row(cls, db_row: Mapping):
"""Create MediaItem object from database row."""
db_row = dict(db_row)
db_row["provider"] = "library"
for key in JSON_KEYS:
if key in db_row and db_row[key] is not None:
db_row[key] = json_loads(db_row[key])
if "favorite" in db_row:
db_row["favorite"] = bool(db_row["favorite"])
db_row["item_id"] = str(db_row["item_id"])
return cls.from_dict(db_row)

def to_db_row(self) -> dict:
"""Create dict from item suitable for db."""

def get_db_value(key, value) -> Any:
"""Transform value for db storage."""
if key in JSON_KEYS:
return json_dumps(value)
return value

return {
key: get_db_value(key, value)
for key, value in self.to_dict().items()
if key
not in [
"item_id",
"provider",
"media_type",
"uri",
"album",
"position",
"track_number",
"disc_number",
]
}

@property
def available(self):
"""Return (calculated) availability."""
Expand All @@ -275,18 +241,6 @@ def image(self) -> MediaItemImage | None:
return None
return next((x for x in self.metadata.images if x.type == ImageType.THUMB), None)

def add_provider_mapping(self, prov_mapping: ProviderMapping) -> None:
"""Add provider ID, overwrite existing entry."""
self.provider_mappings = {
x
for x in self.provider_mappings
if not (
x.item_id == prov_mapping.item_id
and x.provider_instance == prov_mapping.provider_instance
)
}
self.provider_mappings.add(prov_mapping)

def __hash__(self) -> int:
"""Return custom hash."""
return hash(self.uri)
Expand Down Expand Up @@ -334,15 +288,15 @@ def __eq__(self, other: ItemMapping) -> bool:
return self.uri == other.uri


@dataclass
@dataclass(kw_only=True)
class Artist(MediaItem):
"""Model for an artist."""

media_type: MediaType = MediaType.ARTIST
mbid: str | None = None


@dataclass
@dataclass(kw_only=True)
class Album(MediaItem):
"""Model for an album."""

Expand All @@ -354,7 +308,7 @@ class Album(MediaItem):
mbid: str | None = None # release group id


@dataclass
@dataclass(kw_only=True)
class Track(MediaItem):
"""Model for a track."""

Expand All @@ -369,16 +323,6 @@ def __hash__(self):
"""Return custom hash."""
return hash((self.provider, self.item_id))

@property
def image(self) -> MediaItemImage | None:
"""Return (first/random) image/thumb from metadata (if any)."""
if image := super().image:
return image
# fallback to album image (use getattr to guard for ItemMapping)
if self.album:
return getattr(self.album, "image", None)
return None

@property
def has_chapters(self) -> bool:
"""
Expand Down Expand Up @@ -406,38 +350,24 @@ class PlaylistTrack(Track):
position: int # required


@dataclass
@dataclass(kw_only=True)
class Playlist(MediaItem):
"""Model for a playlist."""

media_type: MediaType = MediaType.PLAYLIST
owner: str = ""
is_editable: bool = False

def __hash__(self):
"""Return custom hash."""
return hash((self.provider, self.item_id))


@dataclass
@dataclass(kw_only=True)
class Radio(MediaItem):
"""Model for a radio station."""

media_type: MediaType = MediaType.RADIO
duration: int = 172800

def to_db_row(self) -> dict:
"""Create dict from item suitable for db."""
val = super().to_db_row()
val.pop("duration", None)
return val

def __hash__(self):
"""Return custom hash."""
return hash((self.provider, self.item_id))


@dataclass
@dataclass(kw_only=True)
class BrowseFolder(MediaItem):
"""Representation of a Folder used in Browse (which contains media items)."""

Expand All @@ -448,12 +378,21 @@ class BrowseFolder(MediaItem):
label: str = ""
# subitems of this folder when expanding
items: list[MediaItemType | BrowseFolder] | None = None
provider_mappings: set[ProviderMapping] = field(default_factory=set)

def __post_init__(self):
"""Call after init."""
super().__post_init__()
if not self.path:
self.path = f"{self.provider}://{self.item_id}"
if not self.provider_mappings:
self.provider_mappings.add(
ProviderMapping(
item_id=self.item_id,
provider_domain=self.provider,
provider_instance=self.provider,
)
)


MediaItemType = Artist | Album | Track | Radio | Playlist | BrowseFolder
Expand Down
2 changes: 1 addition & 1 deletion music_assistant/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

API_SCHEMA_VERSION: Final[int] = 23
MIN_SCHEMA_VERSION: Final[int] = 23
DB_SCHEMA_VERSION: Final[int] = 24
DB_SCHEMA_VERSION: Final[int] = 25

ROOT_LOGGER_NAME: Final[str] = "music_assistant"

Expand Down
34 changes: 20 additions & 14 deletions music_assistant/server/controllers/media/albums.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,13 @@ async def _add_library_item(self, item: Album) -> Album:
if item.mbid:
match = {"mbid": item.mbid}
if db_row := await self.mass.music.database.get_row(self.db_table, match):
cur_item = Album.from_db_row(db_row)
cur_item = Album.from_dict(self._parse_db_row(db_row))
# existing item found: update it
return await self.update_item_in_library(cur_item.item_id, item)
# fallback to search and match
match = {"sort_name": item.sort_name}
for row in await self.mass.music.database.get_rows(self.db_table, match):
row_album = Album.from_db_row(row)
for db_row in await self.mass.music.database.get_rows(self.db_table, match):
row_album = Album.from_dict(self._parse_db_row(db_row))
if compare_album(row_album, item):
cur_item = row_album
# existing item found: update it
Expand All @@ -254,7 +254,15 @@ async def _add_library_item(self, item: Album) -> Album:
new_item = await self.mass.music.database.insert(
self.db_table,
{
**item.to_db_row(),
"name": item.name,
"sort_name": item.sort_name,
"version": item.version,
"favorite": item.favorite,
"album_type": item.album_type,
"year": item.year,
"mbid": item.mbid,
"metadata": serialize_to_json(item.metadata),
"provider_mappings": serialize_to_json(item.provider_mappings),
"artists": serialize_to_json(album_artists),
"sort_artist": sort_artist,
"timestamp_added": int(utc_timestamp()),
Expand Down Expand Up @@ -358,16 +366,14 @@ async def _get_db_album_tracks(
db_id = int(item_id) # ensure integer
db_album = await self.get_library_item(db_id)
result: list[AlbumTrack] = []
async for album_track_row in self.mass.music.database.iter_items(
DB_TABLE_ALBUM_TRACKS, {"album_id": db_id}
):
# TODO: make this a nice join query
track_id = album_track_row["track_id"]
track_row = await self.mass.music.database.get_row(
DB_TABLE_TRACKS, {"item_id": track_id}
)
album_track = AlbumTrack.from_db_row(
{**track_row, **album_track_row, "album": db_album.to_dict()}
query = (
f"SELECT * FROM {DB_TABLE_TRACKS} INNER JOIN albumtracks "
"ON albumtracks.track_id = tracks.item_id WHERE albumtracks.album_id = :album_id"
)
track_rows = await self.mass.music.database.get_rows_from_query(query, {"album_id": db_id})
for album_track_row in track_rows:
album_track = AlbumTrack.from_dict(
self._parse_db_row({**album_track_row, "album": db_album.to_dict()})
)
if db_album.metadata.images:
album_track.metadata.images = db_album.metadata.images
Expand Down
Loading