from collections.abc import Callable, Coroutine
from typing import TYPE_CHECKING, Any
-from music_assistant.client.exceptions import (
- ConnectionClosed,
- InvalidServerVersion,
- InvalidState,
-)
+from music_assistant.client.exceptions import ConnectionClosed, InvalidServerVersion, InvalidState
from music_assistant.common.models.api import (
CommandMessage,
ErrorResultMessage,
return None
provider_instance_or_domain = prov.domain
# fallback to match on domain
+ # note that this can be tricky if the provider has multiple instances
+ # and has unique data (e.g. filesystem)
for prov in self._providers.values():
if prov.domain != provider_instance_or_domain:
continue
quality += 1
return quality
- def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]:
- """Execute action(s) on serialization."""
- # prevent sending back unavailable items in the api if a provider has been disabled.
- # by overriding the available flag here.
- if not (available_providers := get_global_cache_value("unique_providers")):
- # this is probably the client
- return d
- if TYPE_CHECKING:
- available_providers = cast(set[str], available_providers)
- if not available_providers.intersection({d["provider_domain"], d["provider_instance"]}):
- d["available"] = False
- return d
-
def __hash__(self) -> int:
"""Return custom hash."""
return hash((self.provider_instance, self.item_id))
type: ImageType
path: str
- provider: str
+ provider: str # provider lookup key (only use instance id for fileproviders)
remotely_accessible: bool = False # url that is accessible from anywhere
def __hash__(self) -> int:
return False
return self.__hash__() == other.__hash__()
- @classmethod
- def __pre_deserialize__(cls, d: dict[Any, Any]) -> dict[Any, Any]:
- """Handle actions before deserialization."""
- # migrate from url provider --> builtin
- # TODO: remove this after 2.0 is launched
- if d["provider"] == "url":
- d["provider"] = "builtin"
- d["remotely_accessible"] = True
- return d
-
@dataclass(frozen=True, kw_only=True)
class MediaItemChapter(DataClassDictMixin):
@property
def available(self) -> bool:
"""Return (calculated) availability."""
- return any(x.available for x in self.provider_mappings)
+ if not (available_providers := get_global_cache_value("unique_providers")):
+ # this is probably the client
+ return any(x.available for x in self.provider_mappings)
+ if TYPE_CHECKING:
+ available_providers = cast(set[str], available_providers)
+ for x in self.provider_mappings:
+ if available_providers.intersection({x.provider_domain, x.provider_instance}):
+ return True
+ return False
@property
def image(self) -> MediaItemImage | None:
)
return library_item
- async def _get_library_item_by_match(self, item: Track) -> int | None:
- if cur_item := await self.get_library_item_by_prov_id(item.item_id, item.provider):
+ async def _get_library_item_by_match(self, item: Track | ItemMapping) -> int | None:
+ if item.provider == "library":
+ return int(item.item_id)
+ # search by provider mappings
+ if isinstance(item, ItemMapping):
+ if cur_item := await self.get_library_item_by_prov_id(item.item_id, item.provider):
+ return cur_item.item_id
+ elif cur_item := await self.get_library_item_by_prov_mappings(item.provider_mappings):
return cur_item.item_id
if cur_item := await self.get_library_item_by_external_ids(item.external_ids):
# existing item match by external id
)
self.manifest.icon = "book-information-variant"
self._reset_online_slots()
- self._scanner_running: bool = False
+ self._scanner_task: asyncio.Task | None = None
async def get_config_entries(
self,
async def close(self) -> None:
"""Handle logic on server stop."""
+ self.stop_metadata_scanner()
self.mass.streams.unregister_dynamic_route("/imageproxy")
@property
if item.media_type == MediaType.RADIO:
await self._update_radio_metadata(item, force_refresh=force_refresh)
- @api_command("metadata/scan")
- async def metadata_scanner(self) -> None:
- """Scanner for (missing) metadata."""
- if self._scanner_running:
+ @api_command("metadata/start_scan")
+ def start_metadata_scanner(self) -> None:
+ """
+ Start scanner for (missing) metadata.
+
+ Usually this is triggered by the music controller after finishing a library sync.
+ """
+ if self._scanner_task and not self._scanner_task.done():
# already running
return
- self._scanner_running = True
- try:
- timestamp = int(time() - 60 * 60 * 24 * 30)
- query = (
- f"WHERE json_extract({DB_TABLE_ARTISTS}.metadata,'$.last_refresh') ISNULL "
- f"OR json_extract({DB_TABLE_ARTISTS}.metadata,'$.last_refresh') < {timestamp}"
- )
- for artist in await self.mass.music.artists.library_items(
- limit=50, order_by="random", extra_query=query
- ):
- await self._update_artist_metadata(artist)
- # we really need to throttle this
- await asyncio.sleep(30)
+ self._scanner_task = self.mass.create_task(self._metadata_scanner())
- query = (
- f"WHERE json_extract({DB_TABLE_ALBUMS}.metadata,'$.last_refresh') ISNULL "
- f"OR json_extract({DB_TABLE_ALBUMS}.metadata,'$.last_refresh') < {timestamp}"
- )
- for album in await self.mass.music.albums.library_items(
- limit=50, order_by="random", extra_query=query
- ):
- await self._update_album_metadata(album)
- # we really need to throttle this
- await asyncio.sleep(30)
-
- query = (
- f"WHERE json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') ISNULL "
- f"OR json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') < {timestamp}"
- )
- for playlist in await self.mass.music.playlists.library_items(
- limit=50, order_by="random", extra_query=query
- ):
- await self._update_playlist_metadata(playlist)
- # we really need to throttle this
- await asyncio.sleep(30)
-
- query = (
- f"WHERE json_extract({DB_TABLE_TRACKS}.metadata,'$.last_refresh') ISNULL "
- f"OR json_extract({DB_TABLE_TRACKS}.metadata,'$.last_refresh') < {timestamp}"
- )
- for track in await self.mass.music.tracks.library_items(
- limit=50, order_by="random", extra_query=query
- ):
- await self._update_track_metadata(track)
- # we really need to throttle this
- await asyncio.sleep(30)
-
- finally:
- self._scanner_running = False
+ @api_command("metadata/stop_scan")
+ def stop_metadata_scanner(self) -> None:
+ """Stop scanner for (missing) metadata."""
+ if self._scanner_task and not self._scanner_task.done():
+ self._scanner_task.cancel()
+ self._scanner_task = None
async def get_image_data_for_item(
self,
)
return None
+ async def _metadata_scanner(self) -> None:
+ """Scanner for (missing) metadata."""
+ timestamp = int(time() - 60 * 60 * 24 * 30)
+ query = (
+ f"WHERE json_extract({DB_TABLE_ARTISTS}.metadata,'$.last_refresh') ISNULL "
+ f"OR json_extract({DB_TABLE_ARTISTS}.metadata,'$.last_refresh') < {timestamp}"
+ )
+ for artist in await self.mass.music.artists.library_items(
+ limit=25, order_by="random", extra_query=query
+ ):
+ await self._update_artist_metadata(artist)
+ # we really need to throttle this
+ await asyncio.sleep(30)
+
+ query = (
+ f"WHERE json_extract({DB_TABLE_ALBUMS}.metadata,'$.last_refresh') ISNULL "
+ f"OR json_extract({DB_TABLE_ALBUMS}.metadata,'$.last_refresh') < {timestamp}"
+ )
+ for album in await self.mass.music.albums.library_items(
+ limit=25, order_by="random", extra_query=query
+ ):
+ await self._update_album_metadata(album)
+ # we really need to throttle this
+ await asyncio.sleep(30)
+
+ query = (
+ f"WHERE json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') ISNULL "
+ f"OR json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') < {timestamp}"
+ )
+ for playlist in await self.mass.music.playlists.library_items(
+ limit=25, order_by="random", extra_query=query
+ ):
+ await self._update_playlist_metadata(playlist)
+ # we really need to throttle this
+ await asyncio.sleep(30)
+
+ query = (
+ f"WHERE json_extract({DB_TABLE_TRACKS}.metadata,'$.last_refresh') ISNULL "
+ f"OR json_extract({DB_TABLE_TRACKS}.metadata,'$.last_refresh') < {timestamp}"
+ )
+ for track in await self.mass.music.tracks.library_items(
+ limit=25, order_by="random", extra_query=query
+ ):
+ await self._update_track_metadata(track)
+ # we really need to throttle this
+ await asyncio.sleep(30)
+
def _reset_online_slots(self) -> None:
self._online_slots_available = MAX_ONLINE_CALLS_PER_DAY
# reschedule self in 24 hours
from music_assistant.common.models.config_entries import CoreConfig
from music_assistant.server.models.music_provider import MusicProvider
+CONF_RESET_DB = "reset_db"
DEFAULT_SYNC_INTERVAL = 3 * 60 # default sync interval in minutes
CONF_SYNC_INTERVAL = "sync_interval"
CONF_DELETED_PROVIDERS = "deleted_providers"
CONF_ADD_LIBRARY_ON_PLAY = "add_library_on_play"
-DB_SCHEMA_VERSION: Final[int] = 5
+DB_SCHEMA_VERSION: Final[int] = 6
class MusicController(CoreController):
values: dict[str, ConfigValueType] | None = None,
) -> tuple[ConfigEntry, ...]:
"""Return all Config Entries for this core module (if any)."""
- return (
+ entries = (
ConfigEntry(
key=CONF_SYNC_INTERVAL,
type=ConfigEntryType.INTEGER,
description="Automatically add a track or radio station to "
"the library when played (if its not already in the library).",
),
+ ConfigEntry(
+ key=CONF_RESET_DB,
+ type=ConfigEntryType.ACTION,
+ label="Reset library database",
+ description="This will issue a full reset of the library "
+ "database and trigger a full sync. Only use this option as a last resort "
+ "if you are seeing issues with the library database.",
+ category="advanced",
+ ),
)
+ if action == CONF_RESET_DB:
+ await self._reset_database()
+ await self.mass.cache.clear()
+ self.start_sync()
+ entries = (
+ *entries,
+ ConfigEntry(
+ key=CONF_RESET_DB,
+ type=ConfigEntryType.LABEL,
+ label="The database has been reset.",
+ ),
+ )
+ return entries
async def setup(self, config: CoreConfig) -> None:
"""Async initialize of module."""
media_type = media_item.media_type
ctrl = self.get_controller(media_type)
- is_library_item = media_item.provider == "library"
+ library_id = media_item.item_id if media_item.provider == "library" else None
- available_providers = get_global_cache_value("provider_instance_ids")
+ available_providers = get_global_cache_value("available_providers")
if TYPE_CHECKING:
available_providers = cast(set[str], available_providers)
# fetch the first (available) provider item
for prov_mapping in media_item.provider_mappings:
- provider = prov_mapping.provider_instance
- if provider not in available_providers:
- continue
- item_id = prov_mapping.item_id
- if prov_mapping.available:
- break
+ if self.mass.get_provider(prov_mapping.provider_instance):
+ with suppress(MediaNotFoundError):
+ media_item = await ctrl.get_provider_item(
+ prov_mapping.item_id, prov_mapping.provider_instance, force_refresh=True
+ )
+ provider = media_item.provider
+ item_id = media_item.item_id
+ break
else:
- # try to find a substitute
+ # try to find a substitute using search
searchresult = await self.search(media_item.name, [media_item.media_type], 20)
if media_item.media_type == MediaType.ARTIST:
result = searchresult.artists
else:
result = searchresult.radio
for item in result:
+ if item == media_item or item.provider == "library":
+ continue
if item.available:
provider = item.provider
item_id = item.item_id
# fetch full (provider) item
media_item = await ctrl.get_provider_item(item_id, provider, force_refresh=True)
# update library item if needed (including refresh of the metadata etc.)
- if is_library_item:
- library_item = await ctrl.add_item_to_library(media_item, overwrite_existing=True)
+ if library_id is not None:
+ library_item = await ctrl.update_item_in_library(library_id, media_item, overwrite=True)
await self.mass.metadata.update_metadata(library_item, force_refresh=True)
return library_item
# schedule db cleanup + metadata scan after sync
if not self.in_progress_syncs:
self.mass.create_task(self._cleanup_database())
- self.mass.create_task(self.mass.metadata.metadata_scanner())
+ self.mass.metadata.start_metadata_scanner()
task.add_done_callback(on_sync_task_done)
await self.__create_database_tables()
return
- if prev_version < 3:
+ if prev_version <= 2:
# convert musicbrainz external id's
await self.database.execute(
f"UPDATE {DB_TABLE_ARTISTS} SET external_ids = "
"replace(external_ids, 'musicbrainz', 'musicbrainz_recordingid')"
)
- if prev_version < 4:
+ if prev_version <= 3:
# remove all additional track provider mappings to cleanup the mess caused
# by a bug that mapped the wrong track artists.
async for track in self.tracks.iter_library_items():
)
await self.tracks.remove_item_from_library(track.item_id)
- if prev_version < 5:
+ if prev_version <= 4:
# remove corrupted provider mappings
for ctrl in (self.artists, self.albums, self.tracks, self.playlists, self.radio):
query = (
}
await ctrl.update_item_in_library(item.item_id, item, True)
+ if prev_version <= 5:
+ # mark all provider mappings as available to recover from the bug
+ # that caused some items to be marked as unavailable
+ await self.database.execute(f"UPDATE {DB_TABLE_PROVIDER_MAPPINGS} SET available = 1")
+ for ctrl in (self.artists, self.albums, self.tracks, self.playlists, self.radio):
+ await self.database.execute(
+ f"UPDATE {ctrl.db_table} SET provider_mappings = "
+ "replace (provider_mappings, '\"available\":false', '\"available\":true')"
+ )
+
+ if prev_version <= 5:
+ # migrate images to lookup key
+ unique_provs = ("filesystem", "jellyfin", "plex", "opensubsonic")
+ for ctrl in (self.artists, self.albums, self.tracks, self.playlists, self.radio):
+ async for item in ctrl.iter_library_items():
+ if not item.metadata or not item.metadata.images:
+ continue
+ changes = False
+ for item in item.metadata.images: # noqa: PLW2901, B020
+ if "--" not in item.provider:
+ continue
+ if item.provider.startswith(unique_provs):
+ continue
+ item.provider = item.provider.split("--")[0]
+ changes = True
+ if changes:
+ await ctrl.update_item_in_library(item.item_id, item, True)
+
# save changes
await self.database.commit()
+ # always clear the cache after a db migration
+ await self.mass.cache.clear()
+
+ async def _reset_database(self) -> None:
+ """Reset the database."""
+ self.mass.metadata.stop_metadata_scanner()
+ await self.close()
+ db_path = os.path.join(self.mass.storage_path, "library.db")
+ await asyncio.to_thread(os.remove, db_path)
+ await self._setup_database()
+
async def __create_database_tables(self) -> None:
"""Create database tables."""
await self.database.execute(
library_item = await controller.update_item_in_library(
library_item.item_id, prov_item
)
+ elif library_item.available != prov_item.available:
+ # existing item availability changed
+ library_item = await controller.update_item_in_library(
+ library_item.item_id, prov_item
+ )
cur_db_ids.add(library_item.item_id)
await asyncio.sleep(0) # yield to eventloop
except MusicAssistantError as err:
MediaItemImage(
type=ImageType.THUMB,
path=artwork["url"].format(w=artwork["width"], h=artwork["height"]),
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=artwork["url"].format(w=artwork["width"], h=artwork["height"]),
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=artwork["url"].format(w=artwork["width"], h=artwork["height"]),
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=url,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=image_url,
- provider=self.instance_id,
+ provider=self.domain,
remotely_accessible=image_url.startswith("http"),
)
]
MediaItemImage(
type=ImageType.THUMB,
path=image_url,
- provider=self.instance_id,
+ provider=self.domain,
remotely_accessible=image_url.startswith("http"),
)
]
MediaItemImage(
type=ImageType.THUMB,
path=image_url,
- provider=self.instance_id,
+ provider=self.domain,
remotely_accessible=image_url.startswith("http"),
)
]
MediaItemImage(
type=ImageType.THUMB,
path=url,
- provider=self.instance_id,
+ provider=self.domain,
remotely_accessible=False,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=track.album.cover_big,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=album.cover_big,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
],
MediaItemImage(
type=ImageType.THUMB,
path=artist.picture_big,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
],
MediaItemImage(
type=ImageType.THUMB,
path=playlist.picture_big,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
],
MediaItemImage(
type=img_type,
path=item["url"],
- provider=self.instance_id,
+ provider=self.domain,
remotely_accessible=True,
)
)
MediaItemImage(
type=img_type,
path=item["url"],
- provider=self.instance_id,
+ provider=self.domain,
remotely_accessible=True,
)
)
MediaItemImage(
type=ImageType.THUMB,
path=img,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=img,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=img,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=img,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
from __future__ import annotations
-from collections.abc import Sequence
-from typing import TYPE_CHECKING
+from collections.abc import AsyncGenerator, Sequence
+from typing import TYPE_CHECKING, cast
from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station
-from music_assistant.common.models.enums import LinkType, ProviderFeature, StreamType
+from music_assistant.common.models.config_entries import ConfigEntry
+from music_assistant.common.models.enums import (
+ ConfigEntryType,
+ LinkType,
+ ProviderFeature,
+ StreamType,
+)
from music_assistant.common.models.errors import MediaNotFoundError
from music_assistant.common.models.media_items import (
AudioFormat,
from music_assistant.server.controllers.cache import use_cache
from music_assistant.server.models.music_provider import MusicProvider
-SUPPORTED_FEATURES = (ProviderFeature.SEARCH, ProviderFeature.BROWSE)
+SUPPORTED_FEATURES = (
+ ProviderFeature.SEARCH,
+ ProviderFeature.BROWSE,
+ # RadioBrowser doesn't support a library feature at all
+ # but MA users like to favorite their radio stations and
+ # have that included in backups so we store it in the config.
+ ProviderFeature.LIBRARY_RADIOS,
+ ProviderFeature.LIBRARY_RADIOS_EDIT,
+)
if TYPE_CHECKING:
- from music_assistant.common.models.config_entries import (
- ConfigEntry,
- ConfigValueType,
- ProviderConfig,
- )
+ from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig
from music_assistant.common.models.provider import ProviderManifest
from music_assistant.server import MusicAssistant
from music_assistant.server.models import ProviderInstanceType
+CONF_STORED_RADIOS = "stored_radios"
+
async def setup(
mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
values: the (intermediate) raw values for config entries sent with the action.
"""
# ruff: noqa: ARG001 D205
- return () # we do not have any config entries (yet)
+ return (
+ ConfigEntry(
+ # RadioBrowser doesn't support a library feature at all
+ # but MA users like to favorite their radio stations and
+ # have that included in backups so we store it in the config.
+ key=CONF_STORED_RADIOS,
+ type=ConfigEntryType.STRING,
+ label=CONF_STORED_RADIOS,
+ default_value=[],
+ required=False,
+ multi_value=True,
+ hidden=True,
+ ),
+ )
class RadioBrowserProvider(MusicProvider):
except RadioBrowserError as err:
self.logger.exception("%s", err)
+ # copy the radiobrowser items that were added to the library
+ # TODO: remove this logic after version 2.3.0 or later
+ if not self.config.get_value(CONF_STORED_RADIOS) and self.mass.music.database:
+ async for db_row in self.mass.music.database.iter_items(
+ "provider_mappings",
+ {"media_type": "radio", "provider_domain": "radiobrowser"},
+ ):
+ await self.library_add(await self.get_radio(db_row["provider_item_id"]))
+
async def search(
self, search_query: str, media_types: list[MediaType], limit: int = 10
) -> SearchResults:
MediaItemImage(
type=ImageType.THUMB,
path=country.favicon,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
return await self.get_by_country(subsubpath)
return []
+ async def get_library_radios(self) -> AsyncGenerator[Radio, None]:
+ """Retrieve library/subscribed radio stations from the provider."""
+ stored_radios = self.config.get_value(CONF_STORED_RADIOS)
+ if TYPE_CHECKING:
+ stored_radios = cast(list[str], stored_radios)
+ for item in stored_radios:
+ yield await self.get_radio(item)
+
+ async def library_add(self, item: MediaItemType) -> bool:
+ """Add item to provider's library. Return true on success."""
+ stored_radios = self.config.get_value(CONF_STORED_RADIOS)
+ if TYPE_CHECKING:
+ stored_radios = cast(list[str], stored_radios)
+ if item.item_id in stored_radios:
+ return False
+ self.logger.debug("Adding radio %s to stored radios", item.item_id)
+ stored_radios = [*stored_radios, item.item_id]
+ await self.mass.config.set_provider_config_value(
+ self.instance_id, CONF_STORED_RADIOS, stored_radios
+ )
+ return True
+
+ async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
+ """Remove item from provider's library. Return true on success."""
+ stored_radios = self.config.get_value(CONF_STORED_RADIOS)
+ if TYPE_CHECKING:
+ stored_radios = cast(list[str], stored_radios)
+ if prov_item_id not in stored_radios:
+ return False
+ self.logger.debug("Removing radio %s from stored radios", prov_item_id)
+ stored_radios = [x for x in stored_radios if x != prov_item_id]
+ await self.mass.config.set_provider_config_value(
+ self.instance_id, CONF_STORED_RADIOS, stored_radios
+ )
+ return True
+
@use_cache(3600 * 24)
async def get_tag_names(self) -> Sequence[str]:
"""Get a list of tag names."""
MediaItemImage(
type=ImageType.THUMB,
path=radio_obj.favicon,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=img_url,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=self._transform_artwork_url(playlist_obj["artwork_url"]),
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=self._transform_artwork_url(track_obj["artwork_url"]),
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path="https://misc.scdn.co/liked-songs/liked-songs-64.png",
- provider=self.domain,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=img_url,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=album_obj["images"][0]["url"],
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=track_obj["album"]["images"][0]["url"],
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=playlist_obj["images"][0]["url"],
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=img_type,
path=img,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
)
MediaItemImage(
type=img_type,
path=img,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
)
MediaItemImage(
type=img_type,
path=img,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
)
MediaItemImage(
type=ImageType.THUMB,
path=image_url,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=image_url,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=image_url,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=image_url,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=ImageType.THUMB,
path=img,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
]
MediaItemImage(
type=image_type,
path=url,
- provider=self.instance_id,
+ provider=self.lookup_key,
remotely_accessible=True,
)
)