import asyncio
from typing import List, Optional, Union
-from music_assistant.helpers.compare import compare_album, compare_artist
+from music_assistant.helpers.compare import compare_album, loose_compare_strings
from music_assistant.helpers.database import TABLE_ALBUMS, TABLE_TRACKS
from music_assistant.helpers.json import json_serializer
from music_assistant.helpers.tags import FALLBACK_ARTIST
from music_assistant.models.enums import EventType, MusicProviderFeature, ProviderType
+from music_assistant.models.errors import MediaNotFoundError
from music_assistant.models.event import MassEvent
from music_assistant.models.media_controller import MediaControllerBase
from music_assistant.models.media_items import (
*[self.search(search_query, prov_type) for prov_type in prov_types]
)
for prov_item in prov_items
- if (
- (prov_item.sort_name in album.sort_name)
- or (album.sort_name in prov_item.sort_name)
- )
- and compare_artist(prov_item.artist, album.artist)
+ if loose_compare_strings(album.name, prov_item.name)
}
# make sure that the 'base' version is included
for prov_version in album.provider_ids:
# return the aggregated result
return all_versions.values()
- async def add(self, item: Album, overwrite_existing: bool = False) -> Album:
+ async def add(self, item: Album) -> Album:
"""Add album to local db and return the database item."""
# grab additional metadata
await self.mass.metadata.get_album_metadata(item)
- db_item = await self.add_db_item(item, overwrite_existing)
+ db_item = await self.add_db_item(item)
# also fetch same album on all providers
await self._match(db_item)
db_item = await self.get_db_item(db_item.item_id)
provider_ids = item.provider_ids
album_artists = await self._get_album_artists(item, overwrite=True)
else:
- metadata = cur_item.metadata.update(item.metadata)
+ metadata = cur_item.metadata.update(item.metadata, item.provider.is_file())
provider_ids = {*cur_item.provider_ids, *item.provider_ids}
album_artists = await self._get_album_artists(item, cur_item)
)
assert not (db_rows and not recursive), "Tracks attached to album"
for db_row in db_rows:
- await self.mass.music.albums.delete_db_item(db_row["item_id"], recursive)
+ try:
+ await self.mass.music.albums.delete_db_item(
+ db_row["item_id"], recursive
+ )
+ except MediaNotFoundError:
+ pass
# delete the album itself from db
await super().delete_db_item(item_id)
from music_assistant.helpers.database import TABLE_ALBUMS, TABLE_ARTISTS, TABLE_TRACKS
from music_assistant.helpers.json import json_serializer
from music_assistant.models.enums import EventType, MusicProviderFeature, ProviderType
+from music_assistant.models.errors import MediaNotFoundError
from music_assistant.models.event import MassEvent
from music_assistant.models.media_controller import MediaControllerBase
from music_assistant.models.media_items import (
final_items[key].in_library = True
return list(final_items.values())
- async def add(self, item: Artist, overwrite_existing: bool = False) -> Artist:
+ async def add(self, item: Artist) -> Artist:
"""Add artist to local db and return the database item."""
# grab musicbrainz id and additional metadata
await self.mass.metadata.get_artist_metadata(item)
- db_item = await self.add_db_item(item, overwrite_existing)
+ db_item = await self.add_db_item(item)
# also fetch same artist on all providers
await self.match_artist(db_item)
db_item = await self.get_db_item(db_item.item_id)
metadata = item.metadata
provider_ids = item.provider_ids
else:
- metadata = cur_item.metadata.update(item.metadata)
+ metadata = cur_item.metadata.update(item.metadata, item.provider.is_file())
provider_ids = {*cur_item.provider_ids, *item.provider_ids}
await self.mass.database.update(
)
assert not (db_rows and not recursive), "Albums attached to artist"
for db_row in db_rows:
- await self.mass.music.albums.delete_db_item(db_row["item_id"], recursive)
+ try:
+ await self.mass.music.albums.delete_db_item(
+ db_row["item_id"], recursive
+ )
+ except MediaNotFoundError:
+ pass
# check artist tracks
db_rows = await self.mass.database.get_rows_from_query(
)
assert not (db_rows and not recursive), "Tracks attached to artist"
for db_row in db_rows:
- await self.mass.music.albums.delete_db_item(db_row["item_id"], recursive)
+ try:
+ await self.mass.music.albums.delete_db_item(
+ db_row["item_id"], recursive
+ )
+ except MediaNotFoundError:
+ pass
# delete the artist itself from db
await super().delete_db_item(item_id)
)
return items
- async def add(self, item: Playlist, overwrite_existing: bool = False) -> Playlist:
+ async def add(self, item: Playlist) -> Playlist:
"""Add playlist to local db and return the new database item."""
item.metadata.last_refresh = int(time())
await self.mass.metadata.get_playlist_metadata(item)
- return await self.add_db_item(item, overwrite_existing)
+ return await self.add_db_item(item)
async def create(
self, name: str, prov_id: Union[ProviderType, str, None] = None
from time import time
from typing import List, Optional
+from music_assistant.helpers.compare import loose_compare_strings
from music_assistant.helpers.database import TABLE_RADIOS
from music_assistant.helpers.json import json_serializer
from music_assistant.models.enums import EventType, MediaType, ProviderType
*[self.search(radio.name, prov_type) for prov_type in prov_types]
)
for prov_item in prov_items
- if (
- (prov_item.name in radio.name)
- or (radio.name in prov_item.name)
- or (prov_item.sort_name in radio.sort_name)
- or (radio.sort_name in prov_item.sort_name)
- )
+ if loose_compare_strings(radio.name, prov_item.name)
}
# make sure that the 'base' version is included
for prov_version in radio.provider_ids:
# return the aggregated result
return all_versions.values()
- async def add(self, item: Radio, overwrite_existing: bool = False) -> Radio:
+ async def add(self, item: Radio) -> Radio:
"""Add radio to local db and return the new database item."""
item.metadata.last_refresh = int(time())
await self.mass.metadata.get_radio_metadata(item)
- return await self.add_db_item(item, overwrite_existing)
+ return await self.add_db_item(item)
async def add_db_item(self, item: Radio, overwrite_existing: bool = False) -> Radio:
"""Add a new item record to the database."""
import asyncio
from typing import List, Optional, Union
-from music_assistant.helpers.compare import compare_artists, compare_track
+from music_assistant.helpers.compare import (
+ compare_artists,
+ compare_track,
+ loose_compare_strings,
+)
from music_assistant.helpers.database import TABLE_TRACKS
from music_assistant.helpers.json import json_serializer
from music_assistant.models.enums import (
track.artists = full_artists
return track
- async def add(self, item: Track, overwrite_existing: bool = False) -> Track:
+ async def add(self, item: Track) -> Track:
"""Add track to local db and return the new database item."""
# make sure we have artists
assert item.artists
# grab additional metadata
await self.mass.metadata.get_track_metadata(item)
- db_item = await self.add_db_item(item, overwrite_existing)
+ db_item = await self.add_db_item(item)
# also fetch same track on all providers (will also get other quality versions)
await self._match(db_item)
return await self.get_db_item(db_item.item_id)
*[self.search(search_query, prov_type) for prov_type in prov_types]
)
for prov_item in prov_items
- if (
- (prov_item.sort_name in track.sort_name)
- or (track.sort_name in prov_item.sort_name)
- )
+ if loose_compare_strings(track.name, prov_item.name)
and compare_artists(prov_item.artists, track.artists, any_match=True)
}
# make sure that the 'base' version is included
track_artists = await self._get_track_artists(item, overwrite=True)
track_albums = await self._get_track_albums(item, overwrite=True)
else:
- metadata = cur_item.metadata.update(item.metadata, overwrite)
+ metadata = cur_item.metadata.update(item.metadata, item.provider.is_file())
provider_ids = {*cur_item.provider_ids, *item.provider_ids}
track_artists = await self._get_track_artists(cur_item, item)
track_albums = await self._get_track_albums(cur_item, item)
)
+def loose_compare_strings(base: str, alt: str) -> bool:
+ """Compare strings and return True even on partial match."""
+ # this is used to display 'versions' of the same track/album
+ # where we account for other spelling or some additional wording in the title
+ word_count = len(base.split(" "))
+ if word_count == 1 and len(base) < 10:
+ return compare_strings(base, alt, False)
+ base_comp = create_safe_string(base)
+ alt_comp = create_safe_string(alt)
+ if base_comp in alt_comp:
+ return True
+ if alt_comp in base_comp:
+ return True
+ return False
+
+
def compare_strings(str1: str, str2: str, strict: bool = True) -> bool:
"""Compare strings and return True if we have an (almost) perfect match."""
if str1 is None or str2 is None:
self._db_add_lock = asyncio.Lock()
@abstractmethod
- async def add(self, item: ItemCls, overwrite_existing: bool = False) -> ItemCls:
+ async def add(self, item: ItemCls) -> ItemCls:
"""Add item to local db and return the database item."""
raise NotImplementedError
force_refresh: bool = False,
lazy: bool = True,
details: ItemCls = None,
- overwrite_existing: bool = None,
) -> ItemCls:
"""Return (full) details for a single media item."""
assert provider or provider_id, "provider or provider_id must be supplied"
provider=provider,
provider_id=provider_id,
)
- if overwrite_existing is None:
- overwrite_existing = force_refresh
if db_item and (time() - db_item.last_refresh) > REFRESH_INTERVAL:
# it's been too long since the full metadata was last retrieved (or never at all)
force_refresh = True
# only if we really need to wait for the result (e.g. to prevent race conditions), we
# can set lazy to false and we await to job to complete.
add_job = self.mass.add_job(
- self.add(details, overwrite_existing=overwrite_existing),
+ self.add(details),
f"Add {details.uri} to database",
)
if not lazy:
await add_job.wait()
return add_job.result
- return db_item if db_item else details
+ return details
async def search(
self,
if item.provider == ProviderType.DATABASE:
# make sure we have a full object
item = await self.get_db_item(item.item_id)
- for prov in item.provider_ids:
- # returns the first provider that is available
- if not prov.available:
- continue
- if self.mass.music.get_provider(prov.prov_id):
- return (prov.prov_id, prov.item_id)
+ for prefer_file in (True, False):
+ for prov in item.provider_ids:
+ # returns the first provider that is available
+ if not prov.available:
+ continue
+ if prefer_file and not prov.prov_type.is_file():
+ continue
+ if self.mass.music.get_provider(prov.prov_id):
+ return (prov.prov_id, prov.item_id)
return None, None
async def get_db_items_by_query(
def update(
self,
new_values: "MediaItemMetadata",
- allow_overwrite: bool = True,
+ allow_overwrite: bool = False,
) -> "MediaItemMetadata":
"""Update metadata (in-place) with new values."""
for fld in fields(self):
self._current_index: Optional[int] = None
self._current_item_elapsed_time: int = 0
self._prev_item: Optional[QueueItem] = None
- self._last_state = str
+ self._last_player_state: Tuple[str, str] = ("", "")
self._items: List[QueueItem] = []
self._save_task: TimerHandle = None
self._last_player_update: int = 0
PlayerState.PAUSED,
):
await self.stop()
- await self._wait_for_state((PlayerState.OFF, PlayerState.IDLE))
self._announcement_in_progress = True
+ await self._wait_for_state((PlayerState.OFF, PlayerState.IDLE))
# turn on player if needed
if not self.player.powered:
if self._announcement_in_progress:
self.logger.warning("Ignore queue command: An announcement is in progress")
return
- if self.active and self.player.state == PlayerState.PAUSED:
+ if self.player.state == PlayerState.PAUSED:
await self.player.play()
else:
await self.resume()
async def resume(self) -> None:
"""Resume previous queue."""
+ last_player_url = self._last_player_state[1]
+ if last_player_url and self.mass.streams.base_url not in last_player_url:
+ self.logger.info("Trying to resume non-MA content %s...", last_player_url)
+ await self.player.play_url(last_player_url)
+ return
resume_item = self.current_item
resume_pos = self._current_item_elapsed_time
if (
def on_player_update(self) -> None:
"""Call when player updates."""
- player_state_str = f"{self.player.state.value}.{self.player.current_url}"
- if self._last_state != player_state_str:
+ cur_player_state = (self.player.state.value, self.player.current_url)
+ if self._last_player_state != cur_player_state:
# playback state changed
- self._last_state = player_state_str
+ if self._announcement_in_progress:
+ # while announcement in progress dont update the last url
+ # to allow us to resume from 3rd party sources
+ # https://github.com/music-assistant/hass-music-assistant/issues/697
+ self._last_player_state = (
+ cur_player_state[0],
+ self._last_player_state[1],
+ )
+ else:
+ self._last_player_state = cur_player_state
# always signal update if playback state changed
self.signal_update()