multi_instance: bool = False
# builtin: whether this provider is a system/builtin and can not disabled/removed
builtin: bool = False
+ # hidden: hide entry in the UI
+ hidden: bool = False
# load_by_default: load this provider by default (mostly used together with `builtin`)
load_by_default: bool = False
# depends_on: depends on another provider to function
@api_command("config/players")
def get_player_configs(self, provider: str | None = None) -> list[PlayerConfig]:
"""Return all known player configurations, optionally filtered by provider domain."""
+ available_providers = {x.domain for x in self.mass.providers}
return [
self.get_player_config(player_id)
for player_id, raw_conf in self.get(CONF_PLAYERS).items()
- if (provider in (None, raw_conf["provider"]))
+ # filter out unavailable providers
+ if raw_conf["provider"] in available_providers
+ # optional provider filter
+ and (provider in (None, raw_conf["provider"]))
]
@api_command("config/players/get")
raw_conf["available"] = False
raw_conf["name"] = raw_conf.get("name")
raw_conf["default_name"] = raw_conf.get("default_name") or raw_conf["player_id"]
- prov_entries = prov.get_player_config_entries(player_id)
prov_entries_keys = {x.key for x in prov_entries}
# combine provider defined entries with default player config entries
entries = prov_entries + tuple(
db_table = DB_TABLE_ALBUMS
media_type = MediaType.ALBUM
item_cls = DbAlbum
+ _db_add_lock = asyncio.Lock()
def __init__(self, *args, **kwargs):
"""Initialize class."""
# grab additional metadata
if not skip_metadata_lookup:
await self.mass.metadata.get_album_metadata(item)
- existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
+ async with self._db_add_lock:
+ # use the lock to prevent a race condition of the same item being added twice
+ existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
if existing:
db_item = await self._update_db_item(existing.item_id, item)
else:
assert item.provider_mappings, "Item is missing provider mapping(s)"
assert item.artists, f"Album {item.name} is missing artists"
cur_item = None
- # always try to grab existing item by musicbrainz_id
- if item.musicbrainz_id:
- match = {"musicbrainz_id": item.musicbrainz_id}
- cur_item = await self.mass.music.database.get_row(self.db_table, match)
- # try barcode/upc
- if not cur_item and item.barcode:
- for barcode in item.barcode:
- if search_result := await self.mass.music.database.search(
- self.db_table, barcode, "barcode"
- ):
- cur_item = Album.from_db_row(search_result[0])
- break
- if not cur_item:
- # fallback to search and match
- for row in await self.mass.music.database.search(self.db_table, item.name):
- row_album = Album.from_db_row(row)
- if compare_album(row_album, item):
- cur_item = row_album
- break
+ # safety guard: check for existing item first
+ # use the lock to prevent a race condition of the same item being added twice
+ async with self._db_add_lock:
+ # always try to grab existing item by musicbrainz_id
+ if item.musicbrainz_id:
+ match = {"musicbrainz_id": item.musicbrainz_id}
+ cur_item = await self.mass.music.database.get_row(self.db_table, match)
+ # try barcode/upc
+ if not cur_item and item.barcode:
+ for barcode in item.barcode:
+ if search_result := await self.mass.music.database.search(
+ self.db_table, barcode, "barcode"
+ ):
+ cur_item = Album.from_db_row(search_result[0])
+ break
+ if not cur_item:
+ # fallback to search and match
+ for row in await self.mass.music.database.search(self.db_table, item.name):
+ row_album = Album.from_db_row(row)
+ if compare_album(row_album, item):
+ cur_item = row_album
+ break
if cur_item:
# update existing
return await self._update_db_item(cur_item.item_id, item)
"timestamp_modified": int(utc_timestamp()),
},
)
- item_id = new_item["item_id"]
- # update/set provider_mappings table
- await self._set_provider_mappings(item_id, item.provider_mappings)
- self.logger.debug("added %s to database", item.name)
+ item_id = new_item["item_id"]
+ # update/set provider_mappings table
+ await self._set_provider_mappings(item_id, item.provider_mappings)
+ self.logger.debug("added %s to database", item.name)
# return created object
return await self.get_db_item(item_id)
else:
album_type = cur_item.album_type
sort_artist = album_artists[0].sort_name if album_artists else ""
- async with self._db_add_lock:
- await self.mass.music.database.update(
- self.db_table,
- {"item_id": db_id},
- {
- "name": item.name if overwrite else cur_item.name,
- "sort_name": item.sort_name if overwrite else cur_item.sort_name,
- "sort_artist": sort_artist,
- "version": item.version if overwrite else cur_item.version,
- "year": item.year if overwrite else cur_item.year or item.year,
- "barcode": ";".join(cur_item.barcode),
- "album_type": album_type.value,
- "artists": serialize_to_json(album_artists) or None,
- "metadata": serialize_to_json(metadata),
- "provider_mappings": serialize_to_json(provider_mappings),
- "musicbrainz_id": item.musicbrainz_id or cur_item.musicbrainz_id,
- "timestamp_modified": int(utc_timestamp()),
- },
- )
- # update/set provider_mappings table
- await self._set_provider_mappings(db_id, provider_mappings)
- self.logger.debug("updated %s in database: %s", item.name, db_id)
+ await self.mass.music.database.update(
+ self.db_table,
+ {"item_id": db_id},
+ {
+ "name": item.name if overwrite else cur_item.name,
+ "sort_name": item.sort_name if overwrite else cur_item.sort_name,
+ "sort_artist": sort_artist,
+ "version": item.version if overwrite else cur_item.version,
+ "year": item.year if overwrite else cur_item.year or item.year,
+ "barcode": ";".join(cur_item.barcode),
+ "album_type": album_type.value,
+ "artists": serialize_to_json(album_artists) or None,
+ "metadata": serialize_to_json(metadata),
+ "provider_mappings": serialize_to_json(provider_mappings),
+ "musicbrainz_id": item.musicbrainz_id or cur_item.musicbrainz_id,
+ "timestamp_modified": int(utc_timestamp()),
+ },
+ )
+ # update/set provider_mappings table
+ await self._set_provider_mappings(db_id, provider_mappings)
+ self.logger.debug("updated %s in database: %s", item.name, db_id)
return await self.get_db_item(db_id)
async def _get_provider_album_tracks(
db_table = DB_TABLE_ARTISTS
media_type = MediaType.ARTIST
item_cls = Artist
+ _db_add_lock = asyncio.Lock()
def __init__(self, *args, **kwargs):
"""Initialize class."""
# grab musicbrainz id and additional metadata
if not skip_metadata_lookup:
await self.mass.metadata.get_artist_metadata(item)
- existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
+ async with self._db_add_lock:
+ # use the lock to prevent a race condition of the same item being added twice
+ existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
if existing:
db_item = await self._update_db_item(existing.item_id, item)
else:
item.musicbrainz_id = VARIOUS_ARTISTS_ID
if item.musicbrainz_id == VARIOUS_ARTISTS_ID:
item.name = VARIOUS_ARTISTS
-
- # always try to grab existing item by musicbrainz_id
- cur_item = None
- if musicbrainz_id := getattr(item, "musicbrainz_id", None):
- match = {"musicbrainz_id": musicbrainz_id}
- cur_item = await self.mass.music.database.get_row(self.db_table, match)
- if not cur_item:
- # fallback to exact name match
- # NOTE: we match an artist by name which could theoretically lead to collisions
- # but the chance is so small it is not worth the additional overhead of grabbing
- # the musicbrainz id upfront
- match = {"sort_name": item.sort_name}
- for row in await self.mass.music.database.get_rows(self.db_table, match):
- row_artist = Artist.from_db_row(row)
- if row_artist.sort_name == item.sort_name:
- cur_item = row_artist
- break
+ # safety guard: check for existing item first
+ # use the lock to prevent a race condition of the same item being added twice
+ async with self._db_add_lock:
+ # always try to grab existing item by musicbrainz_id
+ cur_item = None
+ if musicbrainz_id := getattr(item, "musicbrainz_id", None):
+ match = {"musicbrainz_id": musicbrainz_id}
+ cur_item = await self.mass.music.database.get_row(self.db_table, match)
+ if not cur_item:
+ # fallback to exact name match
+ # NOTE: we match an artist by name which could theoretically lead to collisions
+ # but the chance is so small it is not worth the additional overhead of grabbing
+ # the musicbrainz id upfront
+ match = {"sort_name": item.sort_name}
+ for row in await self.mass.music.database.get_rows(self.db_table, match):
+ row_artist = Artist.from_db_row(row)
+ if row_artist.sort_name == item.sort_name:
+ cur_item = row_artist
+ break
if cur_item:
# update existing
return await self._update_db_item(cur_item.item_id, item)
item = Artist.from_dict(item.to_dict())
async with self._db_add_lock:
new_item = await self.mass.music.database.insert(self.db_table, item.to_db_row())
- item_id = new_item["item_id"]
- # update/set provider_mappings table
- await self._set_provider_mappings(item_id, item.provider_mappings)
- self.logger.debug("added %s to database", item.name)
+ item_id = new_item["item_id"]
+ # update/set provider_mappings table
+ await self._set_provider_mappings(item_id, item.provider_mappings)
+ self.logger.debug("added %s to database", item.name)
# return created object
return await self.get_db_item(item_id)
item.musicbrainz_id = VARIOUS_ARTISTS_ID
if item.musicbrainz_id == VARIOUS_ARTISTS_ID:
item.name = VARIOUS_ARTISTS
- async with self._db_add_lock:
- await self.mass.music.database.update(
- self.db_table,
- {"item_id": db_id},
- {
- "name": item.name if overwrite else cur_item.name,
- "sort_name": item.sort_name if overwrite else cur_item.sort_name,
- "musicbrainz_id": musicbrainz_id,
- "metadata": serialize_to_json(metadata),
- "provider_mappings": serialize_to_json(provider_mappings),
- "timestamp_modified": int(utc_timestamp()),
- },
- )
- # update/set provider_mappings table
- await self._set_provider_mappings(db_id, provider_mappings)
- self.logger.debug("updated %s in database: %s", item.name, db_id)
+ await self.mass.music.database.update(
+ self.db_table,
+ {"item_id": db_id},
+ {
+ "name": item.name if overwrite else cur_item.name,
+ "sort_name": item.sort_name if overwrite else cur_item.sort_name,
+ "musicbrainz_id": musicbrainz_id,
+ "metadata": serialize_to_json(metadata),
+ "provider_mappings": serialize_to_json(provider_mappings),
+ "timestamp_modified": int(utc_timestamp()),
+ },
+ )
+ # update/set provider_mappings table
+ await self._set_provider_mappings(db_id, provider_mappings)
+ self.logger.debug("updated %s in database: %s", item.name, db_id)
return await self.get_db_item(db_id)
async def _get_provider_dynamic_tracks(
media_type: MediaType
item_cls: MediaItemType
db_table: str
+ _db_add_lock = asyncio.Lock()
def __init__(self, mass: MusicAssistant):
"""Initialize class."""
self.mass = mass
self.logger = logging.getLogger(f"{ROOT_LOGGER_NAME}.music.{self.media_type.value}")
- self._db_add_lock = asyncio.Lock()
@abstractmethod
async def add(self, item: ItemCls, skip_metadata_lookup: bool = False) -> ItemCls:
"""Set the in-library bool on a database item."""
db_id = int(item_id) # ensure integer
match = {"item_id": db_id}
- await self.mass.music.database.update(self.db_table, match, {"in_library": in_library})
+ async with self._db_add_lock:
+ await self.mass.music.database.update(self.db_table, match, {"in_library": in_library})
db_item = await self.get_db_item(db_id)
self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, db_item.uri, db_item)
return
# update provider_mappings table
- await self.mass.music.database.delete(
- DB_TABLE_PROVIDER_MAPPINGS,
- {
- "media_type": self.media_type.value,
- "item_id": db_id,
- "provider_instance": provider_instance_id,
- },
- )
+ async with self._db_add_lock:
+ await self.mass.music.database.delete(
+ DB_TABLE_PROVIDER_MAPPINGS,
+ {
+ "media_type": self.media_type.value,
+ "item_id": db_id,
+ "provider_instance": provider_instance_id,
+ },
+ )
# update the item in db (provider_mappings column only)
db_item.provider_mappings = {
}
match = {"item_id": db_id}
if db_item.provider_mappings:
- await self.mass.music.database.update(
- self.db_table,
- match,
- {"provider_mappings": serialize_to_json(db_item.provider_mappings)},
- )
+ async with self._db_add_lock:
+ await self.mass.music.database.update(
+ self.db_table,
+ match,
+ {"provider_mappings": serialize_to_json(db_item.provider_mappings)},
+ )
self.logger.debug("removed provider %s from item id %s", provider_instance_id, db_id)
self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, db_item.uri, db_item)
else:
"""Update the provider_items table for the media item."""
db_id = int(item_id) # ensure integer
# clear all records first
- await self.mass.music.database.delete(
- DB_TABLE_PROVIDER_MAPPINGS,
- {"media_type": self.media_type.value, "item_id": db_id},
- )
- # add entries
- for provider_mapping in provider_mappings:
- await self.mass.music.database.insert_or_replace(
+ async with self._db_add_lock:
+ await self.mass.music.database.delete(
DB_TABLE_PROVIDER_MAPPINGS,
- {
- "media_type": self.media_type.value,
- "item_id": db_id,
- "provider_domain": provider_mapping.provider_domain,
- "provider_instance": provider_mapping.provider_instance,
- "provider_item_id": provider_mapping.item_id,
- },
+ {"media_type": self.media_type.value, "item_id": db_id},
)
+ # add entries
+ for provider_mapping in provider_mappings:
+ await self.mass.music.database.insert_or_replace(
+ DB_TABLE_PROVIDER_MAPPINGS,
+ {
+ "media_type": self.media_type.value,
+ "item_id": db_id,
+ "provider_domain": provider_mapping.provider_domain,
+ "provider_instance": provider_mapping.provider_instance,
+ "provider_item_id": provider_mapping.item_id,
+ },
+ )
def _get_provider_mappings(
self,
"""Manage MediaItems of type Playlist."""
from __future__ import annotations
+import asyncio
import random
from collections.abc import AsyncGenerator
from typing import Any
db_table = DB_TABLE_PLAYLISTS
media_type = MediaType.PLAYLIST
item_cls = Playlist
+ _db_add_lock = asyncio.Lock()
def __init__(self, *args, **kwargs):
"""Initialize class."""
# preload playlist tracks listing (do not load them in the db)
async for track in self.tracks(item.item_id, item.provider):
pass
- existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
+ async with self._db_add_lock:
+ # use the lock to prevent a race condition of the same item being added twice
+ existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
if existing:
db_item = await self._update_db_item(existing.item_id, item)
else:
if provider is None:
raise ProviderUnavailableError("No provider available which allows playlists creation.")
- return await provider.create_playlist(name)
+ # create playlist on the provider
+ prov_playlist = await provider.create_playlist(name)
+ prov_playlist.in_library = True
+ # return db playlist
+ return await self.add(prov_playlist, True)
async def add_playlist_tracks(self, db_playlist_id: str | int, uris: list[str]) -> None:
"""Add multiple tracks to playlist. Creates background tasks to process the action."""
async def _add_db_item(self, item: Playlist) -> Playlist:
"""Add a new record to the database."""
assert item.provider_mappings, "Item is missing provider mapping(s)"
- match = {"name": item.name, "owner": item.owner}
- if cur_item := await self.mass.music.database.get_row(self.db_table, match):
+ cur_item = None
+ # safety guard: check for existing item first
+ # use the lock to prevent a race condition of the same item being added twice
+ async with self._db_add_lock:
+ match = {"sort_name": item.sort_name, "owner": item.owner}
+ cur_item = await self.mass.music.database.get_row(self.db_table, match)
+ if cur_item:
# update existing
return await self._update_db_item(cur_item["item_id"], item)
# insert new item
item.timestamp_modified = int(utc_timestamp())
async with self._db_add_lock:
new_item = await self.mass.music.database.insert(self.db_table, item.to_db_row())
- item_id = new_item["item_id"]
- # update/set provider_mappings table
- await self._set_provider_mappings(item_id, item.provider_mappings)
- self.logger.debug("added %s to database", item.name)
+ item_id = new_item["item_id"]
+ # update/set provider_mappings table
+ await self._set_provider_mappings(item_id, item.provider_mappings)
+ self.logger.debug("added %s to database", item.name)
# return created object
return await self.get_db_item(item_id)
cur_item = await self.get_db_item(db_id)
metadata = cur_item.metadata.update(getattr(item, "metadata", None), overwrite)
provider_mappings = self._get_provider_mappings(cur_item, item, overwrite)
- async with self._db_add_lock:
- await self.mass.music.database.update(
- self.db_table,
- {"item_id": db_id},
- {
- # always prefer name/owner from updated item here
- "name": item.name or cur_item.name,
- "sort_name": item.sort_name or cur_item.sort_name,
- "owner": item.owner or cur_item.sort_name,
- "is_editable": item.is_editable,
- "metadata": serialize_to_json(metadata),
- "provider_mappings": serialize_to_json(provider_mappings),
- "timestamp_modified": int(utc_timestamp()),
- },
- )
- # update/set provider_mappings table
- await self._set_provider_mappings(db_id, provider_mappings)
- self.logger.debug("updated %s in database: %s", item.name, db_id)
+ await self.mass.music.database.update(
+ self.db_table,
+ {"item_id": db_id},
+ {
+ # always prefer name/owner from updated item here
+ "name": item.name or cur_item.name,
+ "sort_name": item.sort_name or cur_item.sort_name,
+ "owner": item.owner or cur_item.sort_name,
+ "is_editable": item.is_editable,
+ "metadata": serialize_to_json(metadata),
+ "provider_mappings": serialize_to_json(provider_mappings),
+ "timestamp_modified": int(utc_timestamp()),
+ },
+ )
+ # update/set provider_mappings table
+ await self._set_provider_mappings(db_id, provider_mappings)
+ self.logger.debug("updated %s in database: %s", item.name, db_id)
return await self.get_db_item(db_id)
async def _get_provider_playlist_tracks(
db_table = DB_TABLE_RADIOS
media_type = MediaType.RADIO
item_cls = Radio
+ _db_add_lock = asyncio.Lock()
def __init__(self, *args, **kwargs):
"""Initialize class."""
async def _add_db_item(self, item: Radio) -> Radio:
"""Add a new item record to the database."""
assert item.provider_mappings, "Item is missing provider mapping(s)"
- match = {"name": item.name}
- if cur_item := await self.mass.music.database.get_row(self.db_table, match):
+ cur_item = None
+ # safety guard: check for existing item first
+ # use the lock to prevent a race condition of the same item being added twice
+ async with self._db_add_lock:
+ match = {"name": item.name}
+ cur_item = await self.mass.music.database.get_row(self.db_table, match)
+ if cur_item:
# update existing
return await self._update_db_item(cur_item["item_id"], item)
# insert new item
item.timestamp_modified = int(utc_timestamp())
async with self._db_add_lock:
new_item = await self.mass.music.database.insert(self.db_table, item.to_db_row())
- item_id = new_item["item_id"]
- # update/set provider_mappings table
- await self._set_provider_mappings(item_id, item.provider_mappings)
- self.logger.debug("added %s to database", item.name)
+ item_id = new_item["item_id"]
+ # update/set provider_mappings table
+ await self._set_provider_mappings(item_id, item.provider_mappings)
+ self.logger.debug("added %s to database", item.name)
# return created object
return await self.get_db_item(item_id)
metadata = cur_item.metadata.update(getattr(item, "metadata", None), overwrite)
provider_mappings = self._get_provider_mappings(cur_item, item, overwrite)
match = {"item_id": db_id}
- async with self._db_add_lock:
- await self.mass.music.database.update(
- self.db_table,
- match,
- {
- # always prefer name from updated item here
- "name": item.name or cur_item.name,
- "sort_name": item.sort_name or cur_item.sort_name,
- "metadata": serialize_to_json(metadata),
- "provider_mappings": serialize_to_json(provider_mappings),
- "timestamp_modified": int(utc_timestamp()),
- },
- )
- # update/set provider_mappings table
- await self._set_provider_mappings(db_id, provider_mappings)
- self.logger.debug("updated %s in database: %s", item.name, db_id)
+ await self.mass.music.database.update(
+ self.db_table,
+ match,
+ {
+ # always prefer name from updated item here
+ "name": item.name or cur_item.name,
+ "sort_name": item.sort_name or cur_item.sort_name,
+ "metadata": serialize_to_json(metadata),
+ "provider_mappings": serialize_to_json(provider_mappings),
+ "timestamp_modified": int(utc_timestamp()),
+ },
+ )
+ # update/set provider_mappings table
+ await self._set_provider_mappings(db_id, provider_mappings)
+ self.logger.debug("updated %s in database: %s", item.name, db_id)
return await self.get_db_item(db_id)
async def _get_provider_dynamic_tracks(
db_table = DB_TABLE_TRACKS
media_type = MediaType.TRACK
item_cls = DbTrack
+ _db_add_lock = asyncio.Lock()
def __init__(self, *args, **kwargs):
"""Initialize class."""
# grab additional metadata
if not skip_metadata_lookup:
await self.mass.metadata.get_track_metadata(item)
- existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
+ async with self._db_add_lock:
+ # use the lock to prevent a race condition of the same item being added twice
+ existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
if existing:
db_item = await self._update_db_item(existing.item_id, item)
else:
assert item.provider_mappings, "Track is missing provider mapping(s)"
cur_item = None
- # always try to grab existing item by external_id
- if item.musicbrainz_id:
- match = {"musicbrainz_id": item.musicbrainz_id}
- cur_item = await self.mass.music.database.get_row(self.db_table, match)
- for isrc in item.isrc:
- if search_result := await self.mass.music.database.search(self.db_table, isrc, "isrc"):
- cur_item = Track.from_db_row(search_result[0])
- break
- if not cur_item:
- # fallback to matching
- match = {"sort_name": item.sort_name}
- for row in await self.mass.music.database.get_rows(self.db_table, match):
- row_track = Track.from_db_row(row)
- if compare_track(row_track, item):
- cur_item = row_track
+ # safety guard: check for existing item first
+ # use the lock to prevent a race condition of the same item being added twice
+ async with self._db_add_lock:
+ # always try to grab existing item by external_id
+ if item.musicbrainz_id:
+ match = {"musicbrainz_id": item.musicbrainz_id}
+ cur_item = await self.mass.music.database.get_row(self.db_table, match)
+ for isrc in item.isrc:
+ if search_result := await self.mass.music.database.search(
+ self.db_table, isrc, "isrc"
+ ):
+ cur_item = Track.from_db_row(search_result[0])
break
+ if not cur_item:
+ # fallback to matching
+ match = {"sort_name": item.sort_name}
+ for row in await self.mass.music.database.get_rows(self.db_table, match):
+ row_track = Track.from_db_row(row)
+ if compare_track(row_track, item):
+ cur_item = row_track
+ break
if cur_item:
# update existing
return await self._update_db_item(cur_item.item_id, item)
"timestamp_modified": int(utc_timestamp()),
},
)
- item_id = new_item["item_id"]
- # update/set provider_mappings table
- await self._set_provider_mappings(item_id, item.provider_mappings)
- # return created object
- self.logger.debug("added %s to database: %s", item.name, item_id)
+ item_id = new_item["item_id"]
+ # update/set provider_mappings table
+ await self._set_provider_mappings(item_id, item.provider_mappings)
+ # return created object
+ self.logger.debug("added %s to database: %s", item.name, item_id)
return await self.get_db_item(item_id)
async def _update_db_item(
cur_item.isrc.update(item.isrc)
track_artists = await self._get_artist_mappings(cur_item, item, overwrite=overwrite)
track_albums = await self._get_track_albums(cur_item, item, overwrite=overwrite)
- async with self._db_add_lock:
- await self.mass.music.database.update(
- self.db_table,
- {"item_id": db_id},
- {
- "name": item.name or cur_item.name,
- "sort_name": item.sort_name or cur_item.sort_name,
- "version": item.version or cur_item.version,
- "duration": getattr(item, "duration", None) or cur_item.duration,
- "artists": serialize_to_json(track_artists),
- "albums": serialize_to_json(track_albums),
- "metadata": serialize_to_json(metadata),
- "provider_mappings": serialize_to_json(provider_mappings),
- "isrc": ";".join(cur_item.isrc),
- "timestamp_modified": int(utc_timestamp()),
- },
- )
- # update/set provider_mappings table
- await self._set_provider_mappings(db_id, provider_mappings)
- self.logger.debug("updated %s in database: %s", item.name, db_id)
+ await self.mass.music.database.update(
+ self.db_table,
+ {"item_id": db_id},
+ {
+ "name": item.name or cur_item.name,
+ "sort_name": item.sort_name or cur_item.sort_name,
+ "version": item.version or cur_item.version,
+ "duration": getattr(item, "duration", None) or cur_item.duration,
+ "artists": serialize_to_json(track_artists),
+ "albums": serialize_to_json(track_albums),
+ "metadata": serialize_to_json(metadata),
+ "provider_mappings": serialize_to_json(provider_mappings),
+ "isrc": ";".join(cur_item.isrc),
+ "timestamp_modified": int(utc_timestamp()),
+ },
+ )
+ # update/set provider_mappings table
+ await self._set_provider_mappings(db_id, provider_mappings)
+ self.logger.debug("updated %s in database: %s", item.name, db_id)
return await self.get_db_item(db_id)
async def _get_track_albums(
# return imageproxy url for images that need to be resolved
# the original path is double encoded
encoded_url = urllib.parse.quote(urllib.parse.quote(image.path))
- return f"{self.mass.webserver.base_url}/imageproxy?path={encoded_url}"
+ return f"{self.mass.webserver.base_url}/imageproxy?path={encoded_url}&provider={image.provider}" # noqa: E501
return image.path
async def get_thumbnail(
from collections.abc import AsyncGenerator
from dataclasses import dataclass
+import cchardet
import xmltodict
from music_assistant.common.helpers.util import parse_title_and_version
playlist_data = b""
async for chunk in self.read_file_content(prov_playlist_id):
playlist_data += chunk
- playlist_data = playlist_data.decode("utf-8")
+ encoding_details = await asyncio.to_thread(cchardet.detect, playlist_data)
+ playlist_data = playlist_data.decode(encoding_details["encoding"])
if ext in ("m3u", "m3u8"):
playlist_lines = await parse_m3u(playlist_data)
playlist_data = b""
async for chunk in self.read_file_content(prov_playlist_id):
playlist_data += chunk
- playlist_data = playlist_data.decode("utf-8")
+ encoding_details = await asyncio.to_thread(cchardet.detect, playlist_data)
+ playlist_data = playlist_data.decode(encoding_details["encoding"])
for uri in prov_track_ids:
playlist_data += f"\n{uri}"
- # write playlist file
+ # write playlist file (always in utf-8)
await self.write_file_content(prov_playlist_id, playlist_data.encode("utf-8"))
async def remove_playlist_tracks(
playlist_data = b""
async for chunk in self.read_file_content(prov_playlist_id):
playlist_data += chunk
- playlist_data.decode("utf-8")
+ encoding_details = await asyncio.to_thread(cchardet.detect, playlist_data)
+ playlist_data = playlist_data.decode(encoding_details["encoding"])
if ext in ("m3u", "m3u8"):
playlist_lines = await parse_m3u(playlist_data)
cur_lines.append(playlist_line)
new_playlist_data = "\n".join(cur_lines)
- # write playlist file
+ # write playlist file (always in utf-8)
await self.write_file_content(prov_playlist_id, new_playlist_data.encode("utf-8"))
async def create_playlist(self, name: str) -> Playlist:
# filename = await self.resolve(f"{name}.m3u")
filename = f"{name}.m3u"
await self.write_file_content(filename, b"")
- playlist = await self.get_playlist(filename)
- db_playlist = await self.mass.music.playlists.add(playlist, skip_metadata_lookup=True)
- return db_playlist
+ return await self.get_playlist(filename)
async def get_stream_details(self, item_id: str) -> StreamDetails:
"""Return the content details for the given track when it will be streamed."""
async def get_stream_details(self, item_id: str) -> StreamDetails:
"""Get streamdetails for a radio station."""
stream = await self.radios.station(uuid=item_id)
- url = stream.url
url_resolved = stream.url_resolved
await self.radios.station_click(uuid=item_id)
return StreamDetails(
item_id=item_id,
content_type=ContentType.try_parse(stream.codec),
media_type=MediaType.RADIO,
- data=url,
+ data=url_resolved,
expires=time() + 24 * 3600,
- direct=url_resolved,
)
async def get_audio_stream(
"documentation": "https://github.com/music-assistant/hass-music-assistant/discussions/categories/music-providers",
"multi_instance": false,
"builtin": true,
+ "hidden": true,
"load_by_default": true,
"icon": "mdi:mdi-web"
}
"documentation": "",
"multi_instance": false,
"builtin": true,
+ "hidden": true,
"load_by_default": true,
"icon": "md:webhook"
}
"python-slugify==8.0.1",
"mashumaro==3.7",
"memory-tempfile==2.2.3",
- "music-assistant-frontend==20230419.0",
+ "music-assistant-frontend==20230420.0",
"pillow==9.5.0",
"unidecode==1.3.6",
"xmltodict==0.13.0",
git+https://github.com/pytube/pytube.git@refs/pull/1501/head
mashumaro==3.7
memory-tempfile==2.2.3
-music-assistant-frontend==20230419.0
+music-assistant-frontend==20230420.0
orjson==3.8.9
pillow==9.5.0
plexapi==4.13.4
# duplicate package without version is safe to ignore
continue
else:
- print("Found requirement without version specifier: %s" % req_str)
+ print("Found requirement without (exact) version specifier: %s" % req_str)
package_name = req_str
existing = final_requirements.get(package_name)