In library rework + Monitored/Unmonitored statuses (#50)
* In library rework + Monitored/Unmonitored statuses * address comments + format
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request, status
|
||||||
from core.exceptions import ClientDisconnectedError
|
from core.exceptions import ClientDisconnectedError
|
||||||
@@ -11,9 +11,12 @@ from services.album_enrichment_service import AlbumEnrichmentService
|
|||||||
from services.navidrome_library_service import NavidromeLibraryService
|
from services.navidrome_library_service import NavidromeLibraryService
|
||||||
from infrastructure.validators import is_unknown_mbid
|
from infrastructure.validators import is_unknown_mbid
|
||||||
from infrastructure.degradation import try_get_degradation_context
|
from infrastructure.degradation import try_get_degradation_context
|
||||||
from infrastructure.msgspec_fastapi import MsgSpecRoute
|
from infrastructure.msgspec_fastapi import AppStruct, MsgSpecBody, MsgSpecRoute
|
||||||
|
|
||||||
import msgspec.structs
|
import msgspec.structs
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter(route_class=MsgSpecRoute, prefix="/albums", tags=["album"])
|
router = APIRouter(route_class=MsgSpecRoute, prefix="/albums", tags=["album"])
|
||||||
|
|
||||||
@@ -151,6 +154,44 @@ async def get_more_by_artist(
|
|||||||
return await discovery_service.get_more_by_artist(artist_id, album_id, count)
|
return await discovery_service.get_more_by_artist(artist_id, album_id, count)
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorRequest(AppStruct):
|
||||||
|
monitored: bool
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{album_id}/monitor")
|
||||||
|
async def set_album_monitored(
|
||||||
|
album_id: str,
|
||||||
|
body: MonitorRequest = MsgSpecBody(MonitorRequest),
|
||||||
|
album_service: AlbumService = Depends(get_album_service),
|
||||||
|
):
|
||||||
|
if is_unknown_mbid(album_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Invalid or unknown album ID: {album_id}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
success = await album_service.set_album_monitored(album_id, body.monitored)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Album not found in Lidarr: {album_id}"
|
||||||
|
)
|
||||||
|
return {"success": True}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid album request"
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.error(f"Failed to update monitoring status: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to update monitoring status"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{album_id}/lastfm", response_model=LastFmAlbumEnrichment)
|
@router.get("/{album_id}/lastfm", response_model=LastFmAlbumEnrichment)
|
||||||
async def get_album_lastfm_enrichment(
|
async def get_album_lastfm_enrichment(
|
||||||
album_id: str,
|
album_id: str,
|
||||||
|
|||||||
@@ -112,11 +112,12 @@ async def get_library_stats(
|
|||||||
async def get_library_mbids(
|
async def get_library_mbids(
|
||||||
library_service: LibraryService = Depends(get_library_service)
|
library_service: LibraryService = Depends(get_library_service)
|
||||||
):
|
):
|
||||||
mbids, requested = await asyncio.gather(
|
mbids, requested, monitored = await asyncio.gather(
|
||||||
library_service.get_library_mbids(),
|
library_service.get_library_mbids(),
|
||||||
library_service.get_requested_mbids(),
|
library_service.get_requested_mbids(),
|
||||||
|
library_service.get_monitored_mbids(),
|
||||||
)
|
)
|
||||||
return LibraryMbidsResponse(mbids=mbids, requested_mbids=requested)
|
return LibraryMbidsResponse(mbids=mbids, requested_mbids=requested, monitored_mbids=monitored)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/grouped", response_model=LibraryGroupedResponse)
|
@router.get("/grouped", response_model=LibraryGroupedResponse)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class AlbumBasicInfo(AppStruct):
|
|||||||
disambiguation: str | None = None
|
disambiguation: str | None = None
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
requested: bool = False
|
requested: bool = False
|
||||||
|
monitored: bool = False
|
||||||
cover_url: str | None = None
|
cover_url: str | None = None
|
||||||
album_thumb_url: str | None = None
|
album_thumb_url: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class DiscoverQueueItemLight(AppStruct):
|
|||||||
cover_url: str | None = None
|
cover_url: str | None = None
|
||||||
is_wildcard: bool = False
|
is_wildcard: bool = False
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
|
monitored: bool = False
|
||||||
|
|
||||||
|
|
||||||
class DiscoverQueueEnrichment(AppStruct):
|
class DiscoverQueueEnrichment(AppStruct):
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class TopAlbum(AppStruct):
|
|||||||
listen_count: int = 0
|
listen_count: int = 0
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
requested: bool = False
|
requested: bool = False
|
||||||
|
monitored: bool = False
|
||||||
cover_url: str | None = None
|
cover_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ class DiscoveryAlbum(AppStruct):
|
|||||||
year: int | None = None
|
year: int | None = None
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
requested: bool = False
|
requested: bool = False
|
||||||
|
monitored: bool = False
|
||||||
cover_url: str | None = None
|
cover_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class HomeAlbum(AppStruct):
|
|||||||
listen_count: int | None = None
|
listen_count: int | None = None
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
requested: bool = False
|
requested: bool = False
|
||||||
|
monitored: bool = False
|
||||||
source: str | None = None
|
source: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class SyncLibraryResponse(AppStruct):
|
|||||||
class LibraryMbidsResponse(AppStruct):
|
class LibraryMbidsResponse(AppStruct):
|
||||||
mbids: list[str] = []
|
mbids: list[str] = []
|
||||||
requested_mbids: list[str] = []
|
requested_mbids: list[str] = []
|
||||||
|
monitored_mbids: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
class LibraryGroupedResponse(AppStruct):
|
class LibraryGroupedResponse(AppStruct):
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class SuggestResult(AppStruct):
|
|||||||
year: int | None = None
|
year: int | None = None
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
requested: bool = False
|
requested: bool = False
|
||||||
|
monitored: bool = False
|
||||||
disambiguation: str | None = None
|
disambiguation: str | None = None
|
||||||
score: int = 0
|
score: int = 0
|
||||||
|
|
||||||
|
|||||||
+4
@@ -154,6 +154,10 @@ def lidarr_requested_mbids_key() -> str:
|
|||||||
return f"{LIDARR_REQUESTED_PREFIX}_mbids"
|
return f"{LIDARR_REQUESTED_PREFIX}_mbids"
|
||||||
|
|
||||||
|
|
||||||
|
def lidarr_monitored_mbids_key() -> str:
|
||||||
|
return f"{LIDARR_PREFIX}monitored_mbids"
|
||||||
|
|
||||||
|
|
||||||
def lidarr_status_key() -> str:
|
def lidarr_status_key() -> str:
|
||||||
return f"{LIDARR_PREFIX}status"
|
return f"{LIDARR_PREFIX}status"
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -18,6 +18,8 @@ def _decode_json(text: str) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
class DiskMetadataCache:
|
class DiskMetadataCache:
|
||||||
|
_CACHE_VERSION = "v3"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
base_path: Path,
|
base_path: Path,
|
||||||
@@ -57,7 +59,7 @@ class DiskMetadataCache:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _cache_hash(identifier: str) -> str:
|
def _cache_hash(identifier: str) -> str:
|
||||||
return hashlib.sha1(identifier.encode()).hexdigest()
|
return hashlib.sha1(f"{DiskMetadataCache._CACHE_VERSION}:{identifier}".encode()).hexdigest()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _meta_path(file_path: Path) -> Path:
|
def _meta_path(file_path: Path) -> Path:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AlbumInfo(AppStruct):
|
|||||||
total_length: int | None = None
|
total_length: int | None = None
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
requested: bool = False
|
requested: bool = False
|
||||||
|
monitored: bool = False
|
||||||
cover_url: str | None = None
|
cover_url: str | None = None
|
||||||
album_thumb_url: str | None = None
|
album_thumb_url: str | None = None
|
||||||
album_back_url: str | None = None
|
album_back_url: str | None = None
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ReleaseItem(AppStruct):
|
|||||||
year: int | None = None
|
year: int | None = None
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
requested: bool = False
|
requested: bool = False
|
||||||
|
monitored: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ArtistInfo(AppStruct):
|
class ArtistInfo(AppStruct):
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class SearchResult(AppStruct):
|
|||||||
year: int | None = None
|
year: int | None = None
|
||||||
in_library: bool = False
|
in_library: bool = False
|
||||||
requested: bool = False
|
requested: bool = False
|
||||||
|
monitored: bool = False
|
||||||
cover_url: str | None = None
|
cover_url: str | None = None
|
||||||
album_thumb_url: str | None = None
|
album_thumb_url: str | None = None
|
||||||
thumb_url: str | None = None
|
thumb_url: str | None = None
|
||||||
|
|||||||
@@ -296,6 +296,20 @@ class LidarrAlbumRepository(LidarrHistoryRepository):
|
|||||||
logger.error(f"Failed to delete album {album_id}: {e}")
|
logger.error(f"Failed to delete album {album_id}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
async def set_monitored(self, album_mbid: str, monitored: bool) -> bool:
|
||||||
|
lidarr_album = await self._get_album_by_foreign_id(album_mbid)
|
||||||
|
if not lidarr_album:
|
||||||
|
return False
|
||||||
|
album_id = lidarr_album.get("id")
|
||||||
|
artist_mbid = (lidarr_album.get("artist", {}).get("foreignArtistId") or "")
|
||||||
|
if not album_id:
|
||||||
|
return False
|
||||||
|
lock = _get_artist_lock(artist_mbid)
|
||||||
|
async with lock:
|
||||||
|
await self._update_album(album_id, {"monitored": monitored})
|
||||||
|
await self._invalidate_album_list_caches()
|
||||||
|
return True
|
||||||
|
|
||||||
async def add_album(self, musicbrainz_id: str, artist_repo) -> dict:
|
async def add_album(self, musicbrainz_id: str, artist_repo) -> dict:
|
||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
if not musicbrainz_id or not isinstance(musicbrainz_id, str):
|
if not musicbrainz_id or not isinstance(musicbrainz_id, str):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import time
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from core.config import Settings
|
from core.config import Settings
|
||||||
from core.exceptions import ExternalServiceError
|
from core.exceptions import ExternalServiceError
|
||||||
from infrastructure.cache.cache_keys import lidarr_raw_albums_key, lidarr_requested_mbids_key, LIDARR_PREFIX
|
from infrastructure.cache.cache_keys import lidarr_raw_albums_key, lidarr_requested_mbids_key, lidarr_monitored_mbids_key, LIDARR_PREFIX
|
||||||
from infrastructure.cache.memory_cache import CacheInterface
|
from infrastructure.cache.memory_cache import CacheInterface
|
||||||
from infrastructure.http.deduplication import get_deduplicator
|
from infrastructure.http.deduplication import get_deduplicator
|
||||||
from infrastructure.resilience.retry import with_retry, CircuitBreaker
|
from infrastructure.resilience.retry import with_retry, CircuitBreaker
|
||||||
@@ -141,6 +141,7 @@ class LidarrBase:
|
|||||||
await self._cache.delete(lidarr_raw_albums_key())
|
await self._cache.delete(lidarr_raw_albums_key())
|
||||||
await self._cache.clear_prefix(f"{LIDARR_PREFIX}library:")
|
await self._cache.clear_prefix(f"{LIDARR_PREFIX}library:")
|
||||||
await self._cache.delete(lidarr_requested_mbids_key())
|
await self._cache.delete(lidarr_requested_mbids_key())
|
||||||
|
await self._cache.delete(lidarr_monitored_mbids_key())
|
||||||
|
|
||||||
async def _post(self, endpoint: str, data: dict[str, Any]) -> Any:
|
async def _post(self, endpoint: str, data: dict[str, Any]) -> Any:
|
||||||
return await self._request("POST", endpoint, json_data=data)
|
return await self._request("POST", endpoint, json_data=data)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from infrastructure.cache.cache_keys import (
|
|||||||
lidarr_artist_mbids_key,
|
lidarr_artist_mbids_key,
|
||||||
lidarr_library_grouped_key,
|
lidarr_library_grouped_key,
|
||||||
lidarr_requested_mbids_key,
|
lidarr_requested_mbids_key,
|
||||||
|
lidarr_monitored_mbids_key,
|
||||||
)
|
)
|
||||||
from .base import LidarrBase
|
from .base import LidarrBase
|
||||||
|
|
||||||
@@ -31,11 +32,17 @@ class LidarrLibraryRepository(LidarrBase):
|
|||||||
|
|
||||||
for item in data:
|
for item in data:
|
||||||
is_monitored = item.get("monitored", False)
|
is_monitored = item.get("monitored", False)
|
||||||
|
statistics = item.get("statistics", {})
|
||||||
|
has_files = statistics.get("trackFileCount", 0) > 0
|
||||||
|
|
||||||
if not is_monitored and not include_unmonitored:
|
if not is_monitored and not include_unmonitored:
|
||||||
filtered_count += 1
|
filtered_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not has_files:
|
||||||
|
filtered_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
artist_data = item.get("artist", {})
|
artist_data = item.get("artist", {})
|
||||||
artist = artist_data.get("artistName", "Unknown")
|
artist = artist_data.get("artistName", "Unknown")
|
||||||
artist_mbid = artist_data.get("foreignArtistId")
|
artist_mbid = artist_data.get("foreignArtistId")
|
||||||
@@ -92,11 +99,17 @@ class LidarrLibraryRepository(LidarrBase):
|
|||||||
|
|
||||||
for item in albums_data:
|
for item in albums_data:
|
||||||
is_monitored = item.get("monitored", False)
|
is_monitored = item.get("monitored", False)
|
||||||
|
statistics = item.get("statistics", {})
|
||||||
|
has_files = statistics.get("trackFileCount", 0) > 0
|
||||||
|
|
||||||
if not is_monitored and not include_unmonitored:
|
if not is_monitored and not include_unmonitored:
|
||||||
filtered_count += 1
|
filtered_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not has_files:
|
||||||
|
filtered_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
artist_data = item.get("artist", {})
|
artist_data = item.get("artist", {})
|
||||||
artist_mbid = artist_data.get("foreignArtistId")
|
artist_mbid = artist_data.get("foreignArtistId")
|
||||||
artist_name = artist_data.get("artistName", "Unknown")
|
artist_name = artist_data.get("artistName", "Unknown")
|
||||||
@@ -139,6 +152,10 @@ class LidarrLibraryRepository(LidarrBase):
|
|||||||
grouped: dict[str, list[LibraryGroupedAlbum]] = {}
|
grouped: dict[str, list[LibraryGroupedAlbum]] = {}
|
||||||
|
|
||||||
for item in data:
|
for item in data:
|
||||||
|
statistics = item.get("statistics", {})
|
||||||
|
if statistics.get("trackFileCount", 0) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
artist = item.get("artist", {}).get("artistName", "Unknown")
|
artist = item.get("artist", {}).get("artistName", "Unknown")
|
||||||
title = item.get("title")
|
title = item.get("title")
|
||||||
year = None
|
year = None
|
||||||
@@ -182,9 +199,6 @@ class LidarrLibraryRepository(LidarrBase):
|
|||||||
data = await self._get_all_albums_raw()
|
data = await self._get_all_albums_raw()
|
||||||
ids: set[str] = set()
|
ids: set[str] = set()
|
||||||
for item in data:
|
for item in data:
|
||||||
if not item.get("monitored", False):
|
|
||||||
continue
|
|
||||||
|
|
||||||
statistics = item.get("statistics", {})
|
statistics = item.get("statistics", {})
|
||||||
track_file_count = statistics.get("trackFileCount", 0)
|
track_file_count = statistics.get("trackFileCount", 0)
|
||||||
if track_file_count == 0:
|
if track_file_count == 0:
|
||||||
@@ -212,7 +226,9 @@ class LidarrLibraryRepository(LidarrBase):
|
|||||||
data = await self._get("/api/v1/artist")
|
data = await self._get("/api/v1/artist")
|
||||||
ids: set[str] = set()
|
ids: set[str] = set()
|
||||||
for item in data:
|
for item in data:
|
||||||
if not item.get("monitored", False):
|
is_monitored = item.get("monitored", False)
|
||||||
|
has_files = item.get("statistics", {}).get("trackFileCount", 0) > 0
|
||||||
|
if not is_monitored and not has_files:
|
||||||
continue
|
continue
|
||||||
mbid = item.get("foreignArtistId") or item.get("mbId")
|
mbid = item.get("foreignArtistId") or item.get("mbId")
|
||||||
if isinstance(mbid, str):
|
if isinstance(mbid, str):
|
||||||
@@ -232,7 +248,7 @@ class LidarrLibraryRepository(LidarrBase):
|
|||||||
|
|
||||||
async def get_monitored_no_files_mbids(self) -> set[str]:
|
async def get_monitored_no_files_mbids(self) -> set[str]:
|
||||||
"""Return monitored Lidarr albums that have no downloaded track files."""
|
"""Return monitored Lidarr albums that have no downloaded track files."""
|
||||||
cache_key = lidarr_requested_mbids_key()
|
cache_key = lidarr_monitored_mbids_key()
|
||||||
|
|
||||||
cached_result = await self._cache.get(cache_key)
|
cached_result = await self._cache.get(cache_key)
|
||||||
if cached_result is not None:
|
if cached_result is not None:
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ class LidarrRepositoryProtocol(Protocol):
|
|||||||
async def get_requested_mbids(self) -> set[str]:
|
async def get_requested_mbids(self) -> set[str]:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
async def get_monitored_no_files_mbids(self) -> set[str]:
|
||||||
|
...
|
||||||
|
|
||||||
async def delete_album(self, album_id: int, delete_files: bool = False) -> bool:
|
async def delete_album(self, album_id: int, delete_files: bool = False) -> bool:
|
||||||
...
|
...
|
||||||
|
|
||||||
@@ -92,3 +95,6 @@ class LidarrRepositoryProtocol(Protocol):
|
|||||||
self, artist_mbid: str, *, monitored: bool, monitor_new_items: str = "none",
|
self, artist_mbid: str, *, monitored: bool, monitor_new_items: str = "none",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
async def set_monitored(self, album_mbid: str, monitored: bool) -> bool:
|
||||||
|
...
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from repositories.protocols import LidarrRepositoryProtocol, MusicBrainzReposito
|
|||||||
from services.preferences_service import PreferencesService
|
from services.preferences_service import PreferencesService
|
||||||
from services.album_utils import parse_year, find_primary_release, get_ranked_releases, extract_artist_info, extract_tracks, extract_label, build_album_basic_info, lidarr_to_basic_info, mb_to_basic_info
|
from services.album_utils import parse_year, find_primary_release, get_ranked_releases, extract_artist_info, extract_tracks, extract_label, build_album_basic_info, lidarr_to_basic_info, mb_to_basic_info
|
||||||
from infrastructure.persistence import LibraryDB
|
from infrastructure.persistence import LibraryDB
|
||||||
from infrastructure.cache.cache_keys import ALBUM_INFO_PREFIX, LIDARR_ALBUM_DETAILS_PREFIX
|
from infrastructure.cache.cache_keys import ALBUM_INFO_PREFIX, LIDARR_ALBUM_DETAILS_PREFIX, ARTIST_INFO_PREFIX, LIDARR_ARTIST_ALBUMS_PREFIX
|
||||||
from infrastructure.cache.memory_cache import CacheInterface
|
from infrastructure.cache.memory_cache import CacheInterface
|
||||||
from infrastructure.cache.disk_cache import DiskMetadataCache
|
from infrastructure.cache.disk_cache import DiskMetadataCache
|
||||||
from infrastructure.cover_urls import prefer_release_group_cover_url
|
from infrastructure.cover_urls import prefer_release_group_cover_url
|
||||||
@@ -166,10 +166,10 @@ class AlbumService:
|
|||||||
await self._disk_cache.set_album(release_group_id, album_info, is_monitored=album_info.in_library, ttl_seconds=ttl if not album_info.in_library else None)
|
await self._disk_cache.set_album(release_group_id, album_info, is_monitored=album_info.in_library, ttl_seconds=ttl if not album_info.in_library else None)
|
||||||
|
|
||||||
def _check_lidarr_in_library(self, lidarr_album: dict | None) -> bool:
|
def _check_lidarr_in_library(self, lidarr_album: dict | None) -> bool:
|
||||||
if lidarr_album and lidarr_album.get("monitored", False):
|
if lidarr_album is None:
|
||||||
statistics = lidarr_album.get("statistics", {})
|
return False
|
||||||
return statistics.get("trackFileCount", 0) > 0
|
statistics = lidarr_album.get("statistics", {})
|
||||||
return False
|
return statistics.get("trackFileCount", 0) > 0
|
||||||
|
|
||||||
async def warm_full_album_cache(self, release_group_id: str) -> None:
|
async def warm_full_album_cache(self, release_group_id: str) -> None:
|
||||||
"""Fire-and-forget: populate the full album_info cache if missing."""
|
"""Fire-and-forget: populate the full album_info cache if missing."""
|
||||||
@@ -192,7 +192,48 @@ class AlbumService:
|
|||||||
|
|
||||||
return await self.get_album_info(release_group_id)
|
return await self.get_album_info(release_group_id)
|
||||||
|
|
||||||
async def get_album_info(self, release_group_id: str, monitored_mbids: set[str] = None) -> AlbumInfo:
|
async def set_album_monitored(self, release_group_id: str, monitored: bool) -> bool:
|
||||||
|
try:
|
||||||
|
release_group_id = validate_mbid(release_group_id, "album")
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Invalid album MBID: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
success = await self._lidarr_repo.set_monitored(release_group_id, monitored)
|
||||||
|
if success:
|
||||||
|
await self._cache.delete(f"{ALBUM_INFO_PREFIX}{release_group_id}")
|
||||||
|
await self._cache.delete(f"{LIDARR_ALBUM_DETAILS_PREFIX}{release_group_id}")
|
||||||
|
await self._disk_cache.delete_album(release_group_id)
|
||||||
|
try:
|
||||||
|
lidarr_album = await self._lidarr_repo.get_album_details(release_group_id)
|
||||||
|
if lidarr_album:
|
||||||
|
artist_data = lidarr_album.get("artist", {})
|
||||||
|
artist_mbid = artist_data.get("foreignArtistId")
|
||||||
|
if artist_mbid:
|
||||||
|
await self._cache.delete(f"{ARTIST_INFO_PREFIX}{artist_mbid}")
|
||||||
|
await self._cache.delete(f"{LIDARR_ARTIST_ALBUMS_PREFIX}{artist_mbid}")
|
||||||
|
await self._disk_cache.delete_artist(artist_mbid)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning(f"Failed to invalidate artist caches for {release_group_id}: {e}")
|
||||||
|
try:
|
||||||
|
existing = await self._library_db.get_album_by_mbid(release_group_id)
|
||||||
|
if existing:
|
||||||
|
album_data: dict = {
|
||||||
|
"mbid": release_group_id,
|
||||||
|
"monitored": monitored,
|
||||||
|
"artist_mbid": existing.get("artist_mbid"),
|
||||||
|
"artist_name": existing.get("artist_name"),
|
||||||
|
"title": existing.get("title"),
|
||||||
|
"year": existing.get("year"),
|
||||||
|
"cover_url": existing.get("cover_url"),
|
||||||
|
"date_added": existing.get("date_added"),
|
||||||
|
}
|
||||||
|
await self._library_db.upsert_album(album_data)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning(f"Failed to update library_db for {release_group_id}: {e}")
|
||||||
|
return success
|
||||||
|
|
||||||
|
async def get_album_info(self, release_group_id: str, library_mbids: set[str] = None) -> AlbumInfo:
|
||||||
try:
|
try:
|
||||||
release_group_id = validate_mbid(release_group_id, "album")
|
release_group_id = validate_mbid(release_group_id, "album")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -215,7 +256,7 @@ class AlbumService:
|
|||||||
future: asyncio.Future[AlbumInfo] = loop.create_future()
|
future: asyncio.Future[AlbumInfo] = loop.create_future()
|
||||||
self._album_in_flight[release_group_id] = future
|
self._album_in_flight[release_group_id] = future
|
||||||
try:
|
try:
|
||||||
album_info = await self._do_get_album_info(release_group_id, cache_key, monitored_mbids)
|
album_info = await self._do_get_album_info(release_group_id, cache_key, library_mbids)
|
||||||
if not future.done():
|
if not future.done():
|
||||||
future.set_result(album_info)
|
future.set_result(album_info)
|
||||||
return album_info
|
return album_info
|
||||||
@@ -232,14 +273,16 @@ class AlbumService:
|
|||||||
raise ResourceNotFoundError(f"Failed to get album info: {e}")
|
raise ResourceNotFoundError(f"Failed to get album info: {e}")
|
||||||
|
|
||||||
async def _do_get_album_info(
|
async def _do_get_album_info(
|
||||||
self, release_group_id: str, cache_key: str, monitored_mbids: set[str] | None
|
self, release_group_id: str, cache_key: str, library_mbids: set[str] | None
|
||||||
) -> AlbumInfo:
|
) -> AlbumInfo:
|
||||||
lidarr_album = await self._lidarr_repo.get_album_details(release_group_id) if self._lidarr_repo.is_configured() else None
|
lidarr_album = await self._lidarr_repo.get_album_details(release_group_id) if self._lidarr_repo.is_configured() else None
|
||||||
in_library = self._check_lidarr_in_library(lidarr_album)
|
in_library = self._check_lidarr_in_library(lidarr_album)
|
||||||
if in_library and lidarr_album:
|
if in_library and lidarr_album:
|
||||||
album_info = await self._build_album_from_lidarr(release_group_id, lidarr_album)
|
album_info = await self._build_album_from_lidarr(release_group_id, lidarr_album)
|
||||||
else:
|
else:
|
||||||
album_info = await self._build_album_from_musicbrainz(release_group_id, monitored_mbids)
|
album_info = await self._build_album_from_musicbrainz(release_group_id, library_mbids)
|
||||||
|
if lidarr_album is not None:
|
||||||
|
album_info.monitored = lidarr_album.get("monitored", False)
|
||||||
album_info = await self._apply_audiodb_album_images(
|
album_info = await self._apply_audiodb_album_images(
|
||||||
album_info, release_group_id, album_info.artist_name, album_info.title,
|
album_info, release_group_id, album_info.artist_name, album_info.title,
|
||||||
allow_fetch=True, is_monitored=album_info.in_library,
|
allow_fetch=True, is_monitored=album_info.in_library,
|
||||||
@@ -286,14 +329,21 @@ class AlbumService:
|
|||||||
disambiguation=cached_album_info.disambiguation,
|
disambiguation=cached_album_info.disambiguation,
|
||||||
in_library=cached_album_info.in_library,
|
in_library=cached_album_info.in_library,
|
||||||
requested=is_requested and not cached_album_info.in_library,
|
requested=is_requested and not cached_album_info.in_library,
|
||||||
|
monitored=cached_album_info.monitored,
|
||||||
cover_url=cached_album_info.cover_url,
|
cover_url=cached_album_info.cover_url,
|
||||||
album_thumb_url=album_thumb,
|
album_thumb_url=album_thumb,
|
||||||
)
|
)
|
||||||
|
|
||||||
lidarr_album = await self._lidarr_repo.get_album_details(release_group_id) if self._lidarr_repo.is_configured() else None
|
lidarr_album = await self._lidarr_repo.get_album_details(release_group_id) if self._lidarr_repo.is_configured() else None
|
||||||
in_library = self._check_lidarr_in_library(lidarr_album)
|
in_library = self._check_lidarr_in_library(lidarr_album)
|
||||||
if lidarr_album and lidarr_album.get("monitored", False):
|
|
||||||
|
is_monitored = False
|
||||||
|
if lidarr_album is not None:
|
||||||
|
is_monitored = lidarr_album.get("monitored", False)
|
||||||
|
|
||||||
|
if lidarr_album:
|
||||||
basic = AlbumBasicInfo(**lidarr_to_basic_info(lidarr_album, release_group_id, in_library, is_requested=is_requested))
|
basic = AlbumBasicInfo(**lidarr_to_basic_info(lidarr_album, release_group_id, in_library, is_requested=is_requested))
|
||||||
|
basic.monitored = is_monitored
|
||||||
if not basic.album_thumb_url:
|
if not basic.album_thumb_url:
|
||||||
basic.album_thumb_url = await self._get_audiodb_album_thumb(
|
basic.album_thumb_url = await self._get_audiodb_album_thumb(
|
||||||
release_group_id, basic.artist_name, basic.title,
|
release_group_id, basic.artist_name, basic.title,
|
||||||
@@ -305,6 +355,7 @@ class AlbumService:
|
|||||||
cached_album = await self._library_db.get_album_by_mbid(release_group_id)
|
cached_album = await self._library_db.get_album_by_mbid(release_group_id)
|
||||||
in_library = cached_album is not None
|
in_library = cached_album is not None
|
||||||
basic = AlbumBasicInfo(**mb_to_basic_info(release_group, release_group_id, in_library, is_requested))
|
basic = AlbumBasicInfo(**mb_to_basic_info(release_group, release_group_id, in_library, is_requested))
|
||||||
|
basic.monitored = is_monitored
|
||||||
basic.album_thumb_url = await self._get_audiodb_album_thumb(
|
basic.album_thumb_url = await self._get_audiodb_album_thumb(
|
||||||
release_group_id, basic.artist_name, basic.title,
|
release_group_id, basic.artist_name, basic.title,
|
||||||
allow_fetch=False,
|
allow_fetch=False,
|
||||||
@@ -338,7 +389,7 @@ class AlbumService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
lidarr_album = await self._lidarr_repo.get_album_details(release_group_id) if self._lidarr_repo.is_configured() else None
|
lidarr_album = await self._lidarr_repo.get_album_details(release_group_id) if self._lidarr_repo.is_configured() else None
|
||||||
in_library = lidarr_album is not None and lidarr_album.get("monitored", False)
|
in_library = self._check_lidarr_in_library(lidarr_album)
|
||||||
|
|
||||||
if in_library and lidarr_album:
|
if in_library and lidarr_album:
|
||||||
album_id = lidarr_album.get("id")
|
album_id = lidarr_album.get("id")
|
||||||
@@ -428,9 +479,9 @@ class AlbumService:
|
|||||||
|
|
||||||
return rg_result
|
return rg_result
|
||||||
|
|
||||||
async def _check_in_library(self, release_group_id: str, monitored_mbids: set[str] = None) -> bool:
|
async def _check_in_library(self, release_group_id: str, library_mbids: set[str] = None) -> bool:
|
||||||
if monitored_mbids is not None:
|
if library_mbids is not None:
|
||||||
return release_group_id.lower() in monitored_mbids
|
return release_group_id.lower() in library_mbids
|
||||||
|
|
||||||
library_mbids = await self._lidarr_repo.get_library_mbids(include_release_ids=True)
|
library_mbids = await self._lidarr_repo.get_library_mbids(include_release_ids=True)
|
||||||
return release_group_id.lower() in library_mbids
|
return release_group_id.lower() in library_mbids
|
||||||
@@ -557,13 +608,14 @@ class AlbumService:
|
|||||||
total_tracks=len(tracks),
|
total_tracks=len(tracks),
|
||||||
total_length=total_length if total_length > 0 else None,
|
total_length=total_length if total_length > 0 else None,
|
||||||
in_library=True,
|
in_library=True,
|
||||||
|
monitored=lidarr_album.get("monitored", False),
|
||||||
cover_url=cover_url,
|
cover_url=cover_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _build_album_from_musicbrainz(
|
async def _build_album_from_musicbrainz(
|
||||||
self,
|
self,
|
||||||
release_group_id: str,
|
release_group_id: str,
|
||||||
monitored_mbids: set[str] = None
|
library_mbids: set[str] = None
|
||||||
) -> AlbumInfo:
|
) -> AlbumInfo:
|
||||||
cached_album = await self._library_db.get_album_by_mbid(release_group_id)
|
cached_album = await self._library_db.get_album_by_mbid(release_group_id)
|
||||||
in_library = cached_album is not None
|
in_library = cached_album is not None
|
||||||
@@ -573,7 +625,7 @@ class AlbumService:
|
|||||||
artist_name, artist_id = extract_artist_info(release_group)
|
artist_name, artist_id = extract_artist_info(release_group)
|
||||||
|
|
||||||
if not in_library:
|
if not in_library:
|
||||||
in_library = await self._check_in_library(release_group_id, monitored_mbids)
|
in_library = await self._check_in_library(release_group_id, library_mbids)
|
||||||
|
|
||||||
basic_info = self._build_basic_info(
|
basic_info = self._build_basic_info(
|
||||||
release_group, release_group_id, artist_name, artist_id, in_library
|
release_group, release_group_id, artist_name, artist_id, in_library
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ def lidarr_to_basic_info(lidarr_album: dict, release_group_id: str, in_library:
|
|||||||
"disambiguation": lidarr_album.get("disambiguation"),
|
"disambiguation": lidarr_album.get("disambiguation"),
|
||||||
"in_library": in_library,
|
"in_library": in_library,
|
||||||
"requested": is_requested and not in_library,
|
"requested": is_requested and not in_library,
|
||||||
|
"monitored": lidarr_album.get("monitored", False),
|
||||||
"cover_url": lidarr_album.get("cover_url"),
|
"cover_url": lidarr_album.get("cover_url"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,5 +147,6 @@ def mb_to_basic_info(release_group: dict, release_group_id: str, in_library: boo
|
|||||||
"disambiguation": release_group.get("disambiguation"),
|
"disambiguation": release_group.get("disambiguation"),
|
||||||
"in_library": in_library,
|
"in_library": in_library,
|
||||||
"requested": is_requested and not in_library,
|
"requested": is_requested and not in_library,
|
||||||
|
"monitored": False,
|
||||||
"cover_url": None,
|
"cover_url": None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,8 +218,15 @@ class ArtistService:
|
|||||||
library_album_mbids: dict[str, Any] | None,
|
library_album_mbids: dict[str, Any] | None,
|
||||||
) -> ArtistInfo:
|
) -> ArtistInfo:
|
||||||
lidarr_artist = await self._lidarr_repo.get_artist_details(artist_id) if self._lidarr_repo.is_configured() else None
|
lidarr_artist = await self._lidarr_repo.get_artist_details(artist_id) if self._lidarr_repo.is_configured() else None
|
||||||
in_library = lidarr_artist is not None and lidarr_artist.get("monitored", False)
|
use_lidarr_data = False
|
||||||
if in_library and lidarr_artist:
|
if lidarr_artist is not None:
|
||||||
|
artist_monitored = lidarr_artist.get("monitored", False)
|
||||||
|
has_albums_with_files = any(
|
||||||
|
a.get("statistics", {}).get("trackFileCount", 0) > 0
|
||||||
|
for a in (lidarr_artist.get("albums") or [])
|
||||||
|
)
|
||||||
|
use_lidarr_data = artist_monitored or has_albums_with_files
|
||||||
|
if use_lidarr_data and lidarr_artist:
|
||||||
artist_info = await self._build_artist_from_lidarr(artist_id, lidarr_artist, library_album_mbids)
|
artist_info = await self._build_artist_from_lidarr(artist_id, lidarr_artist, library_album_mbids)
|
||||||
else:
|
else:
|
||||||
artist_info = await self._build_artist_from_musicbrainz(artist_id, library_artist_mbids, library_album_mbids)
|
artist_info = await self._build_artist_from_musicbrainz(artist_id, library_artist_mbids, library_album_mbids)
|
||||||
@@ -390,11 +397,11 @@ class ArtistService:
|
|||||||
include_extended: bool = True,
|
include_extended: bool = True,
|
||||||
include_releases: bool = True,
|
include_releases: bool = True,
|
||||||
) -> ArtistInfo:
|
) -> ArtistInfo:
|
||||||
mb_artist, library_mbids, album_mbids, requested_mbids = await self._fetch_artist_data(
|
mb_artist, library_mbids, album_mbids, requested_mbids, monitored_mbids = await self._fetch_artist_data(
|
||||||
artist_id, library_artist_mbids, library_album_mbids
|
artist_id, library_artist_mbids, library_album_mbids
|
||||||
)
|
)
|
||||||
in_library = artist_id.lower() in library_mbids
|
in_library = artist_id.lower() in library_mbids
|
||||||
albums, singles, eps = (await self._get_categorized_releases(mb_artist, album_mbids, requested_mbids)) if include_releases else ([], [], [])
|
albums, singles, eps = (await self._get_categorized_releases(mb_artist, album_mbids, requested_mbids, monitored_mbids)) if include_releases else ([], [], [])
|
||||||
description, image = (await self._fetch_wikidata_info(mb_artist)) if include_extended else (None, None)
|
description, image = (await self._fetch_wikidata_info(mb_artist)) if include_extended else (None, None)
|
||||||
info = build_base_artist_info(
|
info = build_base_artist_info(
|
||||||
mb_artist, artist_id, in_library,
|
mb_artist, artist_id, in_library,
|
||||||
@@ -440,9 +447,10 @@ class ArtistService:
|
|||||||
if not self._lidarr_repo.is_configured():
|
if not self._lidarr_repo.is_configured():
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
library_mbids, requested_mbids, artist_mbids = await asyncio.gather(
|
library_mbids, requested_mbids, monitored_mbids, artist_mbids = await asyncio.gather(
|
||||||
self._lidarr_repo.get_library_mbids(include_release_ids=False),
|
self._lidarr_repo.get_library_mbids(include_release_ids=False),
|
||||||
self._lidarr_repo.get_requested_mbids(),
|
self._lidarr_repo.get_requested_mbids(),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
self._lidarr_repo.get_artist_mbids(),
|
self._lidarr_repo.get_artist_mbids(),
|
||||||
)
|
)
|
||||||
for release_list in (artist_info.albums, artist_info.singles, artist_info.eps):
|
for release_list in (artist_info.albums, artist_info.singles, artist_info.eps):
|
||||||
@@ -452,6 +460,7 @@ class ArtistService:
|
|||||||
continue
|
continue
|
||||||
rg.in_library = rg_id in library_mbids
|
rg.in_library = rg_id in library_mbids
|
||||||
rg.requested = rg_id in requested_mbids and not rg.in_library
|
rg.requested = rg_id in requested_mbids and not rg.in_library
|
||||||
|
rg.monitored = rg_id in monitored_mbids
|
||||||
mbid_lower = artist_info.musicbrainz_id.lower()
|
mbid_lower = artist_info.musicbrainz_id.lower()
|
||||||
is_in_artist_mbids = mbid_lower in artist_mbids
|
is_in_artist_mbids = mbid_lower in artist_mbids
|
||||||
artist_info.in_library = is_in_artist_mbids
|
artist_info.in_library = is_in_artist_mbids
|
||||||
@@ -530,12 +539,20 @@ class ArtistService:
|
|||||||
) -> ArtistReleases:
|
) -> ArtistReleases:
|
||||||
try:
|
try:
|
||||||
lidarr_artist = await self._lidarr_repo.get_artist_details(artist_id)
|
lidarr_artist = await self._lidarr_repo.get_artist_details(artist_id)
|
||||||
in_library = lidarr_artist is not None and lidarr_artist.get("monitored", False)
|
has_lidarr_data = False
|
||||||
|
if lidarr_artist is not None:
|
||||||
|
artist_monitored = lidarr_artist.get("monitored", False)
|
||||||
|
has_albums_with_files = any(
|
||||||
|
a.get("statistics", {}).get("trackFileCount", 0) > 0
|
||||||
|
for a in (lidarr_artist.get("albums") or [])
|
||||||
|
)
|
||||||
|
has_lidarr_data = artist_monitored or has_albums_with_files
|
||||||
|
|
||||||
album_mbids, requested_mbids, cache_mbids = await asyncio.gather(
|
album_mbids, requested_mbids, cache_mbids, monitored_mbids = await asyncio.gather(
|
||||||
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
||||||
self._lidarr_repo.get_requested_mbids(),
|
self._lidarr_repo.get_requested_mbids(),
|
||||||
self._get_library_cache_mbids(),
|
self._get_library_cache_mbids(),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
)
|
)
|
||||||
album_mbids = album_mbids | cache_mbids
|
album_mbids = album_mbids | cache_mbids
|
||||||
|
|
||||||
@@ -543,7 +560,7 @@ class ArtistService:
|
|||||||
included_primary_types = set(t.lower() for t in prefs.primary_types)
|
included_primary_types = set(t.lower() for t in prefs.primary_types)
|
||||||
included_secondary_types = set(t.lower() for t in prefs.secondary_types)
|
included_secondary_types = set(t.lower() for t in prefs.secondary_types)
|
||||||
|
|
||||||
if in_library:
|
if has_lidarr_data:
|
||||||
if offset == 0:
|
if offset == 0:
|
||||||
lidarr_albums = await self._lidarr_repo.get_artist_albums(artist_id)
|
lidarr_albums = await self._lidarr_repo.get_artist_albums(artist_id)
|
||||||
albums, singles, eps = self._categorize_lidarr_albums(lidarr_albums, album_mbids, requested_mbids=requested_mbids)
|
albums, singles, eps = self._categorize_lidarr_albums(lidarr_albums, album_mbids, requested_mbids=requested_mbids)
|
||||||
@@ -567,7 +584,7 @@ class ArtistService:
|
|||||||
|
|
||||||
return await self._filter_aware_release_page(
|
return await self._filter_aware_release_page(
|
||||||
artist_id, offset, limit, album_mbids, requested_mbids,
|
artist_id, offset, limit, album_mbids, requested_mbids,
|
||||||
included_primary_types, included_secondary_types,
|
included_primary_types, included_secondary_types, monitored_mbids,
|
||||||
)
|
)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.error(f"Error fetching releases for artist {artist_id} at offset {offset}: {e}")
|
logger.error(f"Error fetching releases for artist {artist_id} at offset {offset}: {e}")
|
||||||
@@ -586,6 +603,7 @@ class ArtistService:
|
|||||||
requested_mbids: set[str],
|
requested_mbids: set[str],
|
||||||
included_primary_types: set[str],
|
included_primary_types: set[str],
|
||||||
included_secondary_types: set[str],
|
included_secondary_types: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> ArtistReleases:
|
) -> ArtistReleases:
|
||||||
if not included_primary_types:
|
if not included_primary_types:
|
||||||
return ArtistReleases(
|
return ArtistReleases(
|
||||||
@@ -618,7 +636,7 @@ class ArtistService:
|
|||||||
temp_artist = {"release-group-list": release_groups}
|
temp_artist = {"release-group-list": release_groups}
|
||||||
page_albums, page_singles, page_eps = categorize_release_groups(
|
page_albums, page_singles, page_eps = categorize_release_groups(
|
||||||
temp_artist, album_mbids, included_primary_types,
|
temp_artist, album_mbids, included_primary_types,
|
||||||
included_secondary_types, requested_mbids,
|
included_secondary_types, requested_mbids, monitored_mbids,
|
||||||
)
|
)
|
||||||
|
|
||||||
for item in page_albums:
|
for item in page_albums:
|
||||||
@@ -669,24 +687,27 @@ class ArtistService:
|
|||||||
artist_id: str,
|
artist_id: str,
|
||||||
library_artist_mbids: set[str] = None,
|
library_artist_mbids: set[str] = None,
|
||||||
library_album_mbids: dict[str, Any] = None
|
library_album_mbids: dict[str, Any] = None
|
||||||
) -> tuple[dict, set[str], set[str], set[str]]:
|
) -> tuple[dict, set[str], set[str], set[str], set[str]]:
|
||||||
if library_artist_mbids is not None and library_album_mbids is not None:
|
if library_artist_mbids is not None and library_album_mbids is not None:
|
||||||
mb_artist = await self._mb_repo.get_artist_by_id(artist_id)
|
mb_artist = await self._mb_repo.get_artist_by_id(artist_id)
|
||||||
library_mbids = library_artist_mbids
|
library_mbids = library_artist_mbids
|
||||||
album_mbids = library_album_mbids
|
album_mbids = library_album_mbids
|
||||||
requested_result = await asyncio.gather(
|
requested_result, monitored_result = await asyncio.gather(
|
||||||
self._lidarr_repo.get_requested_mbids(),
|
self._lidarr_repo.get_requested_mbids(),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
requested_mbids = requested_result[0] if not isinstance(requested_result[0], BaseException) else set()
|
requested_mbids = requested_result if not isinstance(requested_result, BaseException) else set()
|
||||||
if isinstance(requested_result[0], BaseException):
|
monitored_mbids = monitored_result if not isinstance(monitored_result, BaseException) else set()
|
||||||
logger.warning(f"Lidarr unavailable, proceeding without requested data: {requested_result[0]}")
|
if isinstance(requested_result, BaseException):
|
||||||
|
logger.warning(f"Lidarr unavailable, proceeding without requested data: {requested_result}")
|
||||||
else:
|
else:
|
||||||
mb_artist, *lidarr_results = await asyncio.gather(
|
mb_artist, *lidarr_results = await asyncio.gather(
|
||||||
self._mb_repo.get_artist_by_id(artist_id),
|
self._mb_repo.get_artist_by_id(artist_id),
|
||||||
self._lidarr_repo.get_artist_mbids(),
|
self._lidarr_repo.get_artist_mbids(),
|
||||||
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
||||||
self._lidarr_repo.get_requested_mbids(),
|
self._lidarr_repo.get_requested_mbids(),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
if isinstance(mb_artist, BaseException):
|
if isinstance(mb_artist, BaseException):
|
||||||
@@ -698,16 +719,15 @@ class ArtistService:
|
|||||||
library_mbids = lidarr_results[0] if not isinstance(lidarr_results[0], BaseException) else set()
|
library_mbids = lidarr_results[0] if not isinstance(lidarr_results[0], BaseException) else set()
|
||||||
album_mbids = lidarr_results[1] if not isinstance(lidarr_results[1], BaseException) else set()
|
album_mbids = lidarr_results[1] if not isinstance(lidarr_results[1], BaseException) else set()
|
||||||
requested_mbids = lidarr_results[2] if not isinstance(lidarr_results[2], BaseException) else set()
|
requested_mbids = lidarr_results[2] if not isinstance(lidarr_results[2], BaseException) else set()
|
||||||
|
monitored_mbids = lidarr_results[3] if not isinstance(lidarr_results[3], BaseException) else set()
|
||||||
|
|
||||||
# Supplement with LibraryDB so monitored albums (even with trackFileCount=0)
|
|
||||||
# are recognised as "in library", consistent with the Library page.
|
|
||||||
cache_mbids = await self._get_library_cache_mbids()
|
cache_mbids = await self._get_library_cache_mbids()
|
||||||
album_mbids = album_mbids | cache_mbids
|
album_mbids = album_mbids | cache_mbids
|
||||||
|
|
||||||
if not mb_artist:
|
if not mb_artist:
|
||||||
raise ResourceNotFoundError("Artist not found")
|
raise ResourceNotFoundError("Artist not found")
|
||||||
|
|
||||||
return mb_artist, library_mbids, album_mbids, requested_mbids
|
return mb_artist, library_mbids, album_mbids, requested_mbids, monitored_mbids
|
||||||
|
|
||||||
def _build_external_links(self, mb_artist: dict[str, Any]) -> list[ExternalLink]:
|
def _build_external_links(self, mb_artist: dict[str, Any]) -> list[ExternalLink]:
|
||||||
external_links_data = extract_external_links(mb_artist)
|
external_links_data = extract_external_links(mb_artist)
|
||||||
@@ -720,7 +740,8 @@ class ArtistService:
|
|||||||
self,
|
self,
|
||||||
mb_artist: dict[str, Any],
|
mb_artist: dict[str, Any],
|
||||||
album_mbids: set[str],
|
album_mbids: set[str],
|
||||||
requested_mbids: set[str] = None
|
requested_mbids: set[str] = None,
|
||||||
|
monitored_mbids: set[str] = None,
|
||||||
) -> tuple[list[ReleaseItem], list[ReleaseItem], list[ReleaseItem]]:
|
) -> tuple[list[ReleaseItem], list[ReleaseItem], list[ReleaseItem]]:
|
||||||
prefs = self._preferences_service.get_preferences()
|
prefs = self._preferences_service.get_preferences()
|
||||||
included_primary_types = set(t.lower() for t in prefs.primary_types)
|
included_primary_types = set(t.lower() for t in prefs.primary_types)
|
||||||
@@ -730,7 +751,8 @@ class ArtistService:
|
|||||||
album_mbids,
|
album_mbids,
|
||||||
included_primary_types,
|
included_primary_types,
|
||||||
included_secondary_types,
|
included_secondary_types,
|
||||||
requested_mbids or set()
|
requested_mbids or set(),
|
||||||
|
monitored_mbids or set(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _fetch_wikidata_info(self, mb_artist: dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
|
async def _fetch_wikidata_info(self, mb_artist: dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
|
||||||
|
|||||||
@@ -106,11 +106,14 @@ def categorize_release_groups(
|
|||||||
included_primary_types: Optional[set[str]] = None,
|
included_primary_types: Optional[set[str]] = None,
|
||||||
included_secondary_types: Optional[set[str]] = None,
|
included_secondary_types: Optional[set[str]] = None,
|
||||||
requested_mbids: Optional[set[str]] = None,
|
requested_mbids: Optional[set[str]] = None,
|
||||||
|
monitored_mbids: Optional[set[str]] = None,
|
||||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
||||||
if included_primary_types is None:
|
if included_primary_types is None:
|
||||||
included_primary_types = {"album", "single", "ep", "broadcast", "other"}
|
included_primary_types = {"album", "single", "ep", "broadcast", "other"}
|
||||||
if requested_mbids is None:
|
if requested_mbids is None:
|
||||||
requested_mbids = set()
|
requested_mbids = set()
|
||||||
|
if monitored_mbids is None:
|
||||||
|
monitored_mbids = set()
|
||||||
albums: list[ReleaseItem] = []
|
albums: list[ReleaseItem] = []
|
||||||
singles: list[ReleaseItem] = []
|
singles: list[ReleaseItem] = []
|
||||||
eps: list[ReleaseItem] = []
|
eps: list[ReleaseItem] = []
|
||||||
@@ -130,6 +133,7 @@ def categorize_release_groups(
|
|||||||
rg_id_lower = rg_id.lower() if rg_id else ""
|
rg_id_lower = rg_id.lower() if rg_id else ""
|
||||||
in_library = rg_id_lower in album_mbids if rg_id else False
|
in_library = rg_id_lower in album_mbids if rg_id else False
|
||||||
requested = rg_id_lower in requested_mbids if rg_id and not in_library else False
|
requested = rg_id_lower in requested_mbids if rg_id and not in_library else False
|
||||||
|
is_monitored = rg_id_lower in monitored_mbids if rg_id and not in_library and not requested else False
|
||||||
rg_data = ReleaseItem(
|
rg_data = ReleaseItem(
|
||||||
id=rg_id,
|
id=rg_id,
|
||||||
title=rg.get("title"),
|
title=rg.get("title"),
|
||||||
@@ -137,6 +141,7 @@ def categorize_release_groups(
|
|||||||
first_release_date=rg.get("first-release-date"),
|
first_release_date=rg.get("first-release-date"),
|
||||||
in_library=in_library,
|
in_library=in_library,
|
||||||
requested=requested,
|
requested=requested,
|
||||||
|
monitored=is_monitored,
|
||||||
)
|
)
|
||||||
if date := rg_data.first_release_date:
|
if date := rg_data.first_release_date:
|
||||||
try:
|
try:
|
||||||
@@ -182,6 +187,7 @@ def categorize_lidarr_albums(
|
|||||||
track_file_count = album.get("track_file_count", 0)
|
track_file_count = album.get("track_file_count", 0)
|
||||||
in_library = track_file_count > 0 or (mbid_lower in _cache_mbids)
|
in_library = track_file_count > 0 or (mbid_lower in _cache_mbids)
|
||||||
requested = mbid_lower in _requested_mbids and not in_library
|
requested = mbid_lower in _requested_mbids and not in_library
|
||||||
|
is_monitored = album.get("monitored", False) and not in_library and not requested
|
||||||
album_data = ReleaseItem(
|
album_data = ReleaseItem(
|
||||||
id=mbid,
|
id=mbid,
|
||||||
title=album.get("title"),
|
title=album.get("title"),
|
||||||
@@ -190,6 +196,7 @@ def categorize_lidarr_albums(
|
|||||||
year=album.get("year"),
|
year=album.get("year"),
|
||||||
in_library=in_library,
|
in_library=in_library,
|
||||||
requested=requested,
|
requested=requested,
|
||||||
|
monitored=is_monitored,
|
||||||
)
|
)
|
||||||
if album_type == "album":
|
if album_type == "album":
|
||||||
albums.append(album_data)
|
albums.append(album_data)
|
||||||
|
|||||||
@@ -167,6 +167,13 @@ class DiscoverHomepageService:
|
|||||||
|
|
||||||
library_mbids = await self._mbid.get_library_artist_mbids(lidarr_configured)
|
library_mbids = await self._mbid.get_library_artist_mbids(lidarr_configured)
|
||||||
|
|
||||||
|
monitored_mbids: set[str] = set()
|
||||||
|
if lidarr_configured:
|
||||||
|
try:
|
||||||
|
monitored_mbids = await self._lidarr_repo.get_monitored_no_files_mbids()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.debug("Failed to fetch monitored MBIDs for discover page")
|
||||||
|
|
||||||
seed_artists = await self._get_seed_artists(
|
seed_artists = await self._get_seed_artists(
|
||||||
lb_enabled, username, jf_enabled,
|
lb_enabled, username, jf_enabled,
|
||||||
resolved_source=resolved_source,
|
resolved_source=resolved_source,
|
||||||
@@ -214,8 +221,8 @@ class DiscoverHomepageService:
|
|||||||
tasks["jf_most_played"] = self._jf_repo.get_most_played_artists(limit=50)
|
tasks["jf_most_played"] = self._jf_repo.get_most_played_artists(limit=50)
|
||||||
|
|
||||||
if lidarr_configured:
|
if lidarr_configured:
|
||||||
tasks["library_artists"] = self._lidarr_repo.get_artists_from_library()
|
tasks["library_artists"] = self._lidarr_repo.get_artists_from_library(include_unmonitored=True)
|
||||||
tasks["library_albums"] = self._lidarr_repo.get_library()
|
tasks["library_albums"] = self._lidarr_repo.get_library(include_unmonitored=True)
|
||||||
|
|
||||||
results = await self._execute_tasks(tasks)
|
results = await self._execute_tasks(tasks)
|
||||||
|
|
||||||
@@ -231,15 +238,15 @@ class DiscoverHomepageService:
|
|||||||
)
|
)
|
||||||
await self._enrich_because_sections_audiodb(response.because_you_listen_to)
|
await self._enrich_because_sections_audiodb(response.because_you_listen_to)
|
||||||
|
|
||||||
response.fresh_releases = self._build_fresh_releases(results, library_mbids)
|
response.fresh_releases = self._build_fresh_releases(results, library_mbids, monitored_mbids)
|
||||||
|
|
||||||
post_tasks: dict[str, Any] = {
|
post_tasks: dict[str, Any] = {
|
||||||
"missing_essentials": self._build_missing_essentials(results, library_mbids),
|
"missing_essentials": self._build_missing_essentials(results, library_mbids, monitored_mbids),
|
||||||
"lastfm_weekly_album_chart": self._build_lastfm_weekly_album_chart(
|
"lastfm_weekly_album_chart": self._build_lastfm_weekly_album_chart(
|
||||||
results, library_mbids
|
results, library_mbids, monitored_mbids
|
||||||
),
|
),
|
||||||
"lastfm_recent_scrobbles": self._build_lastfm_recent_scrobbles(
|
"lastfm_recent_scrobbles": self._build_lastfm_recent_scrobbles(
|
||||||
results, library_mbids
|
results, library_mbids, monitored_mbids
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
if resolved_source == "listenbrainz" and lb_enabled and username:
|
if resolved_source == "listenbrainz" and lb_enabled and username:
|
||||||
@@ -464,7 +471,8 @@ class DiscoverHomepageService:
|
|||||||
return sections
|
return sections
|
||||||
|
|
||||||
def _build_fresh_releases(
|
def _build_fresh_releases(
|
||||||
self, results: dict[str, Any], library_mbids: set[str]
|
self, results: dict[str, Any], library_mbids: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> HomeSection | None:
|
) -> HomeSection | None:
|
||||||
releases = results.get("lb_fresh")
|
releases = results.get("lb_fresh")
|
||||||
if not releases:
|
if not releases:
|
||||||
@@ -475,16 +483,22 @@ class DiscoverHomepageService:
|
|||||||
if isinstance(r, dict):
|
if isinstance(r, dict):
|
||||||
mbid = r.get("release_group_mbid", "")
|
mbid = r.get("release_group_mbid", "")
|
||||||
artist_mbids = r.get("artist_mbids", [])
|
artist_mbids = r.get("artist_mbids", [])
|
||||||
|
in_lib = mbid.lower() in library_mbids if isinstance(mbid, str) and mbid else False
|
||||||
|
is_monitored = (
|
||||||
|
not in_lib and bool(monitored_mbids) and isinstance(mbid, str) and mbid
|
||||||
|
and mbid.lower() in monitored_mbids
|
||||||
|
)
|
||||||
items.append(HomeAlbum(
|
items.append(HomeAlbum(
|
||||||
mbid=mbid,
|
mbid=mbid,
|
||||||
name=r.get("title", r.get("release_group_name", "Unknown")),
|
name=r.get("title", r.get("release_group_name", "Unknown")),
|
||||||
artist_name=r.get("artist_credit_name", r.get("artist_name", "")),
|
artist_name=r.get("artist_credit_name", r.get("artist_name", "")),
|
||||||
artist_mbid=artist_mbids[0] if artist_mbids else None,
|
artist_mbid=artist_mbids[0] if artist_mbids else None,
|
||||||
listen_count=r.get("listen_count"),
|
listen_count=r.get("listen_count"),
|
||||||
in_library=mbid.lower() in library_mbids if isinstance(mbid, str) and mbid else False,
|
in_library=in_lib,
|
||||||
|
monitored=is_monitored,
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
items.append(self._transformers.lb_release_to_home(r, library_mbids))
|
items.append(self._transformers.lb_release_to_home(r, library_mbids, monitored_mbids))
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.debug(f"Skipping fresh release item: {e}")
|
logger.debug(f"Skipping fresh release item: {e}")
|
||||||
continue
|
continue
|
||||||
@@ -498,7 +512,8 @@ class DiscoverHomepageService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _build_missing_essentials(
|
async def _build_missing_essentials(
|
||||||
self, results: dict[str, Any], library_mbids: set[str]
|
self, results: dict[str, Any], library_mbids: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> HomeSection | None:
|
) -> HomeSection | None:
|
||||||
library_artists = results.get("library_artists") or []
|
library_artists = results.get("library_artists") or []
|
||||||
library_albums = results.get("library_albums") or []
|
library_albums = results.get("library_albums") or []
|
||||||
@@ -551,6 +566,7 @@ class DiscoverHomepageService:
|
|||||||
artist_name=rg.artist_name,
|
artist_name=rg.artist_name,
|
||||||
listen_count=rg.listen_count,
|
listen_count=rg.listen_count,
|
||||||
in_library=False,
|
in_library=False,
|
||||||
|
monitored=bool(monitored_mbids) and rg_mbid.lower() in monitored_mbids,
|
||||||
))
|
))
|
||||||
artist_missing += 1
|
artist_missing += 1
|
||||||
|
|
||||||
@@ -904,6 +920,7 @@ class DiscoverHomepageService:
|
|||||||
self,
|
self,
|
||||||
results: dict[str, Any],
|
results: dict[str, Any],
|
||||||
library_mbids: set[str],
|
library_mbids: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> HomeSection | None:
|
) -> HomeSection | None:
|
||||||
albums = results.get("lfm_weekly_albums") or []
|
albums = results.get("lfm_weekly_albums") or []
|
||||||
if not albums:
|
if not albums:
|
||||||
@@ -914,7 +931,7 @@ class DiscoverHomepageService:
|
|||||||
|
|
||||||
items = []
|
items = []
|
||||||
for album in albums[:20]:
|
for album in albums[:20]:
|
||||||
home_album = self._transformers.lastfm_album_to_home(album, library_mbids)
|
home_album = self._transformers.lastfm_album_to_home(album, library_mbids, monitored_mbids)
|
||||||
if home_album and home_album.mbid:
|
if home_album and home_album.mbid:
|
||||||
home_album.mbid = rg_map.get(home_album.mbid, home_album.mbid)
|
home_album.mbid = rg_map.get(home_album.mbid, home_album.mbid)
|
||||||
items.append(home_album)
|
items.append(home_album)
|
||||||
@@ -933,6 +950,7 @@ class DiscoverHomepageService:
|
|||||||
self,
|
self,
|
||||||
results: dict[str, Any],
|
results: dict[str, Any],
|
||||||
library_mbids: set[str],
|
library_mbids: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> HomeSection | None:
|
) -> HomeSection | None:
|
||||||
tracks = results.get("lfm_recent") or []
|
tracks = results.get("lfm_recent") or []
|
||||||
if not tracks:
|
if not tracks:
|
||||||
@@ -944,7 +962,7 @@ class DiscoverHomepageService:
|
|||||||
items = []
|
items = []
|
||||||
seen_album_mbids: set[str] = set()
|
seen_album_mbids: set[str] = set()
|
||||||
for track in tracks[:30]:
|
for track in tracks[:30]:
|
||||||
home_album = self._transformers.lastfm_recent_to_home(track, library_mbids)
|
home_album = self._transformers.lastfm_recent_to_home(track, library_mbids, monitored_mbids)
|
||||||
if home_album and home_album.mbid:
|
if home_album and home_album.mbid:
|
||||||
resolved = rg_map.get(home_album.mbid, home_album.mbid)
|
resolved = rg_map.get(home_album.mbid, home_album.mbid)
|
||||||
home_album.mbid = resolved
|
home_album.mbid = resolved
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class MbidResolutionService:
|
|||||||
if not lidarr_configured:
|
if not lidarr_configured:
|
||||||
return set()
|
return set()
|
||||||
try:
|
try:
|
||||||
artists = await self._lidarr_repo.get_artists_from_library()
|
artists = await self._lidarr_repo.get_artists_from_library(include_unmonitored=True)
|
||||||
return {a.get("mbid", "").lower() for a in artists if a.get("mbid")}
|
return {a.get("mbid", "").lower() for a in artists if a.get("mbid")}
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
logger.warning("Failed to fetch library artists from Lidarr")
|
logger.warning("Failed to fetch library artists from Lidarr")
|
||||||
|
|||||||
@@ -109,15 +109,17 @@ class HomeChartsService:
|
|||||||
self, genre: str, limit: int = 100, artist_offset: int = 0, album_offset: int = 0
|
self, genre: str, limit: int = 100, artist_offset: int = 0, album_offset: int = 0
|
||||||
) -> GenreDetailResponse:
|
) -> GenreDetailResponse:
|
||||||
lidarr_results = await asyncio.gather(
|
lidarr_results = await asyncio.gather(
|
||||||
self._lidarr_repo.get_artists_from_library(),
|
self._lidarr_repo.get_artists_from_library(include_unmonitored=True),
|
||||||
self._lidarr_repo.get_library(),
|
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
lidarr_failed = any(isinstance(r, BaseException) for r in lidarr_results)
|
lidarr_failed = any(isinstance(r, BaseException) for r in lidarr_results[:2])
|
||||||
if lidarr_failed:
|
if lidarr_failed:
|
||||||
logger.warning("Lidarr unavailable for genre '%s', proceeding with MusicBrainz data only", genre)
|
logger.warning("Lidarr unavailable for genre '%s', proceeding with MusicBrainz data only", genre)
|
||||||
library_artists = lidarr_results[0] if not isinstance(lidarr_results[0], BaseException) else []
|
library_artists = lidarr_results[0] if not isinstance(lidarr_results[0], BaseException) else []
|
||||||
library_albums = lidarr_results[1] if not isinstance(lidarr_results[1], BaseException) else []
|
library_albums = lidarr_results[1] if not isinstance(lidarr_results[1], BaseException) else []
|
||||||
|
monitored_mbids = lidarr_results[2] if not isinstance(lidarr_results[2], BaseException) else set()
|
||||||
library_mbids = {a.get("mbid", "").lower() for a in library_artists if a.get("mbid")}
|
library_mbids = {a.get("mbid", "").lower() for a in library_artists if a.get("mbid")}
|
||||||
library_album_mbids = {a.musicbrainz_id.lower() for a in library_albums if a.musicbrainz_id}
|
library_album_mbids = {a.musicbrainz_id.lower() for a in library_albums if a.musicbrainz_id}
|
||||||
library_section = None
|
library_section = None
|
||||||
@@ -177,6 +179,7 @@ class HomeChartsService:
|
|||||||
image_url=None,
|
image_url=None,
|
||||||
release_date=str(result.year) if result.year else None,
|
release_date=str(result.year) if result.year else None,
|
||||||
in_library=result.musicbrainz_id.lower() in library_album_mbids,
|
in_library=result.musicbrainz_id.lower() in library_album_mbids,
|
||||||
|
monitored=result.musicbrainz_id.lower() not in library_album_mbids and result.musicbrainz_id.lower() in monitored_mbids,
|
||||||
)
|
)
|
||||||
for result in mb_album_results
|
for result in mb_album_results
|
||||||
]
|
]
|
||||||
@@ -203,7 +206,7 @@ class HomeChartsService:
|
|||||||
if resolved == "lastfm" and self._lfm_repo:
|
if resolved == "lastfm" and self._lfm_repo:
|
||||||
return await self._get_trending_artists_lastfm(limit)
|
return await self._get_trending_artists_lastfm(limit)
|
||||||
|
|
||||||
library_artists = await self._lidarr_repo.get_artists_from_library()
|
library_artists = await self._lidarr_repo.get_artists_from_library(include_unmonitored=True)
|
||||||
library_mbids = {a.get("mbid", "").lower() for a in library_artists if a.get("mbid")}
|
library_mbids = {a.get("mbid", "").lower() for a in library_artists if a.get("mbid")}
|
||||||
ranges = ["this_week", "this_month", "this_year", "all_time"]
|
ranges = ["this_week", "this_month", "this_year", "all_time"]
|
||||||
tasks = {r: self._lb_repo.get_sitewide_top_artists(range_=r, count=limit + 1) for r in ranges}
|
tasks = {r: self._lb_repo.get_sitewide_top_artists(range_=r, count=limit + 1) for r in ranges}
|
||||||
@@ -249,7 +252,7 @@ class HomeChartsService:
|
|||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
library_artists, lb_artists = await asyncio.gather(
|
library_artists, lb_artists = await asyncio.gather(
|
||||||
self._lidarr_repo.get_artists_from_library(),
|
self._lidarr_repo.get_artists_from_library(include_unmonitored=True),
|
||||||
self._lb_repo.get_sitewide_top_artists(
|
self._lb_repo.get_sitewide_top_artists(
|
||||||
range_=range_key, count=limit + 1, offset=offset
|
range_=range_key, count=limit + 1, offset=offset
|
||||||
),
|
),
|
||||||
@@ -275,15 +278,19 @@ class HomeChartsService:
|
|||||||
if resolved == "lastfm" and self._lfm_repo:
|
if resolved == "lastfm" and self._lfm_repo:
|
||||||
return await self._get_popular_albums_lastfm(limit)
|
return await self._get_popular_albums_lastfm(limit)
|
||||||
|
|
||||||
library_albums = await self._lidarr_repo.get_library()
|
library_albums = await self._lidarr_repo.get_library(include_unmonitored=True)
|
||||||
library_mbids = {(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id}
|
library_mbids = {(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id}
|
||||||
|
try:
|
||||||
|
monitored_mbids = await self._lidarr_repo.get_monitored_no_files_mbids()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
monitored_mbids = set()
|
||||||
ranges = ["this_week", "this_month", "this_year", "all_time"]
|
ranges = ["this_week", "this_month", "this_year", "all_time"]
|
||||||
tasks = {r: self._lb_repo.get_sitewide_top_release_groups(range_=r, count=limit + 1) for r in ranges}
|
tasks = {r: self._lb_repo.get_sitewide_top_release_groups(range_=r, count=limit + 1) for r in ranges}
|
||||||
results = await self._execute_tasks(tasks)
|
results = await self._execute_tasks(tasks)
|
||||||
response_data = {}
|
response_data = {}
|
||||||
for r in ranges:
|
for r in ranges:
|
||||||
lb_albums = results.get(r) or []
|
lb_albums = results.get(r) or []
|
||||||
albums = [self._transformers.lb_release_to_home(a, library_mbids) for a in lb_albums]
|
albums = [self._transformers.lb_release_to_home(a, library_mbids, monitored_mbids) for a in lb_albums]
|
||||||
featured = albums[0] if albums else None
|
featured = albums[0] if albums else None
|
||||||
items = albums[1:limit] if len(albums) > 1 else []
|
items = albums[1:limit] if len(albums) > 1 else []
|
||||||
response_data[r] = PopularTimeRange(
|
response_data[r] = PopularTimeRange(
|
||||||
@@ -317,14 +324,17 @@ class HomeChartsService:
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
library_albums, lb_albums = await asyncio.gather(
|
library_albums, lb_albums, monitored_mbids_result = await asyncio.gather(
|
||||||
self._lidarr_repo.get_library(),
|
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||||
self._lb_repo.get_sitewide_top_release_groups(
|
self._lb_repo.get_sitewide_top_release_groups(
|
||||||
range_=range_key, count=limit + 1, offset=offset
|
range_=range_key, count=limit + 1, offset=offset
|
||||||
),
|
),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
library_mbids = {(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id}
|
library_mbids = {(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id}
|
||||||
albums = [self._transformers.lb_release_to_home(a, library_mbids) for a in lb_albums]
|
monitored_mbids = monitored_mbids_result if not isinstance(monitored_mbids_result, BaseException) else set()
|
||||||
|
albums = [self._transformers.lb_release_to_home(a, library_mbids, monitored_mbids) for a in lb_albums]
|
||||||
has_more = len(albums) > limit
|
has_more = len(albums) > limit
|
||||||
items = albums[:limit]
|
items = albums[:limit]
|
||||||
return PopularAlbumsRangeResponse(
|
return PopularAlbumsRangeResponse(
|
||||||
@@ -337,7 +347,7 @@ class HomeChartsService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _get_trending_artists_lastfm(self, limit: int = 10) -> TrendingArtistsResponse:
|
async def _get_trending_artists_lastfm(self, limit: int = 10) -> TrendingArtistsResponse:
|
||||||
library_artists = await self._lidarr_repo.get_artists_from_library()
|
library_artists = await self._lidarr_repo.get_artists_from_library(include_unmonitored=True)
|
||||||
library_mbids = {a.get("mbid", "").lower() for a in library_artists if a.get("mbid")}
|
library_mbids = {a.get("mbid", "").lower() for a in library_artists if a.get("mbid")}
|
||||||
lfm_artists = await self._lfm_repo.get_global_top_artists(limit=limit + 1)
|
lfm_artists = await self._lfm_repo.get_global_top_artists(limit=limit + 1)
|
||||||
artists = [
|
artists = [
|
||||||
@@ -366,10 +376,15 @@ class HomeChartsService:
|
|||||||
|
|
||||||
async def _get_popular_albums_lastfm(self, limit: int = 10) -> PopularAlbumsResponse:
|
async def _get_popular_albums_lastfm(self, limit: int = 10) -> PopularAlbumsResponse:
|
||||||
ranges = ["this_week", "this_month", "this_year", "all_time"]
|
ranges = ["this_week", "this_month", "this_year", "all_time"]
|
||||||
library_albums = await self._lidarr_repo.get_library()
|
library_albums, monitored_mbids_result = await asyncio.gather(
|
||||||
|
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
library_mbids = {
|
library_mbids = {
|
||||||
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
||||||
}
|
}
|
||||||
|
monitored_mbids = monitored_mbids_result if not isinstance(monitored_mbids_result, BaseException) else set()
|
||||||
lfm_username = self._get_lastfm_username()
|
lfm_username = self._get_lastfm_username()
|
||||||
if lfm_username:
|
if lfm_username:
|
||||||
tasks = {
|
tasks = {
|
||||||
@@ -408,6 +423,7 @@ class HomeChartsService:
|
|||||||
image_url=album.image_url or None,
|
image_url=album.image_url or None,
|
||||||
listen_count=album.playcount,
|
listen_count=album.playcount,
|
||||||
in_library=(album.mbid or "").lower() in library_mbids if album.mbid else False,
|
in_library=(album.mbid or "").lower() in library_mbids if album.mbid else False,
|
||||||
|
monitored=not ((album.mbid or "").lower() in library_mbids) and (album.mbid or "").lower() in monitored_mbids if album.mbid else False,
|
||||||
source="lastfm",
|
source="lastfm",
|
||||||
)
|
)
|
||||||
for album in lfm_albums
|
for album in lfm_albums
|
||||||
@@ -433,7 +449,7 @@ class HomeChartsService:
|
|||||||
total_to_fetch = min(limit + offset + 1, 200)
|
total_to_fetch = min(limit + offset + 1, 200)
|
||||||
lfm_artists, library_artists = await asyncio.gather(
|
lfm_artists, library_artists = await asyncio.gather(
|
||||||
self._lfm_repo.get_global_top_artists(limit=total_to_fetch),
|
self._lfm_repo.get_global_top_artists(limit=total_to_fetch),
|
||||||
self._lidarr_repo.get_artists_from_library(),
|
self._lidarr_repo.get_artists_from_library(include_unmonitored=True),
|
||||||
)
|
)
|
||||||
library_mbids = {a.get("mbid", "").lower() for a in library_artists if a.get("mbid")}
|
library_mbids = {a.get("mbid", "").lower() for a in library_artists if a.get("mbid")}
|
||||||
artists = [
|
artists = [
|
||||||
@@ -470,17 +486,20 @@ class HomeChartsService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
total_to_fetch = min(limit + offset + 1, 200)
|
total_to_fetch = min(limit + offset + 1, 200)
|
||||||
lfm_albums, library_albums = await asyncio.gather(
|
lfm_albums, library_albums, monitored_mbids_result = await asyncio.gather(
|
||||||
self._lfm_repo.get_user_top_albums(
|
self._lfm_repo.get_user_top_albums(
|
||||||
lfm_username,
|
lfm_username,
|
||||||
period=self._lastfm_period_for_range(range_key),
|
period=self._lastfm_period_for_range(range_key),
|
||||||
limit=total_to_fetch,
|
limit=total_to_fetch,
|
||||||
),
|
),
|
||||||
self._lidarr_repo.get_library(),
|
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
library_mbids = {
|
library_mbids = {
|
||||||
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
||||||
}
|
}
|
||||||
|
monitored_mbids = monitored_mbids_result if not isinstance(monitored_mbids_result, BaseException) else set()
|
||||||
albums = [
|
albums = [
|
||||||
HomeAlbum(
|
HomeAlbum(
|
||||||
mbid=album.mbid,
|
mbid=album.mbid,
|
||||||
@@ -490,6 +509,7 @@ class HomeChartsService:
|
|||||||
image_url=album.image_url or None,
|
image_url=album.image_url or None,
|
||||||
listen_count=album.playcount,
|
listen_count=album.playcount,
|
||||||
in_library=(album.mbid or "").lower() in library_mbids if album.mbid else False,
|
in_library=(album.mbid or "").lower() in library_mbids if album.mbid else False,
|
||||||
|
monitored=not ((album.mbid or "").lower() in library_mbids) and (album.mbid or "").lower() in monitored_mbids if album.mbid else False,
|
||||||
source="lastfm",
|
source="lastfm",
|
||||||
)
|
)
|
||||||
for album in lfm_albums
|
for album in lfm_albums
|
||||||
@@ -521,10 +541,15 @@ class HomeChartsService:
|
|||||||
this_week=empty, this_month=empty, this_year=empty, all_time=empty
|
this_week=empty, this_month=empty, this_year=empty, all_time=empty
|
||||||
)
|
)
|
||||||
|
|
||||||
library_albums = await self._lidarr_repo.get_library()
|
library_albums, monitored_mbids_result = await asyncio.gather(
|
||||||
|
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
library_mbids = {
|
library_mbids = {
|
||||||
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
||||||
}
|
}
|
||||||
|
monitored_mbids = monitored_mbids_result if not isinstance(monitored_mbids_result, BaseException) else set()
|
||||||
ranges = ["this_week", "this_month", "this_year", "all_time"]
|
ranges = ["this_week", "this_month", "this_year", "all_time"]
|
||||||
tasks = {
|
tasks = {
|
||||||
r: self._lb_repo.get_user_top_release_groups(
|
r: self._lb_repo.get_user_top_release_groups(
|
||||||
@@ -536,7 +561,7 @@ class HomeChartsService:
|
|||||||
response_data: dict[str, PopularTimeRange] = {}
|
response_data: dict[str, PopularTimeRange] = {}
|
||||||
for r in ranges:
|
for r in ranges:
|
||||||
rgs = results.get(r) or []
|
rgs = results.get(r) or []
|
||||||
albums = [self._transformers.lb_release_to_home(rg, library_mbids) for rg in rgs]
|
albums = [self._transformers.lb_release_to_home(rg, library_mbids, monitored_mbids) for rg in rgs]
|
||||||
response_data[r] = PopularTimeRange(
|
response_data[r] = PopularTimeRange(
|
||||||
range_key=r,
|
range_key=r,
|
||||||
label=HomeDataTransformers.get_range_label(r),
|
label=HomeDataTransformers.get_range_label(r),
|
||||||
@@ -578,16 +603,19 @@ class HomeChartsService:
|
|||||||
has_more=False,
|
has_more=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
library_albums, rgs = await asyncio.gather(
|
library_albums, rgs, monitored_mbids_result = await asyncio.gather(
|
||||||
self._lidarr_repo.get_library(),
|
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||||
self._lb_repo.get_user_top_release_groups(
|
self._lb_repo.get_user_top_release_groups(
|
||||||
username=lb_username, range_=range_key, count=limit + 1, offset=offset
|
username=lb_username, range_=range_key, count=limit + 1, offset=offset
|
||||||
),
|
),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
library_mbids = {
|
library_mbids = {
|
||||||
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
||||||
}
|
}
|
||||||
albums = [self._transformers.lb_release_to_home(rg, library_mbids) for rg in rgs]
|
monitored_mbids = monitored_mbids_result if not isinstance(monitored_mbids_result, BaseException) else set()
|
||||||
|
albums = [self._transformers.lb_release_to_home(rg, library_mbids, monitored_mbids) for rg in rgs]
|
||||||
has_more = len(albums) > limit
|
has_more = len(albums) > limit
|
||||||
items = albums[:limit]
|
items = albums[:limit]
|
||||||
return PopularAlbumsRangeResponse(
|
return PopularAlbumsRangeResponse(
|
||||||
|
|||||||
@@ -162,9 +162,10 @@ class HomeService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if lidarr_configured:
|
if lidarr_configured:
|
||||||
tasks["library_albums"] = self._lidarr_repo.get_library()
|
tasks["library_albums"] = self._lidarr_repo.get_library(include_unmonitored=True)
|
||||||
tasks["library_artists"] = self._lidarr_repo.get_artists_from_library()
|
tasks["library_artists"] = self._lidarr_repo.get_artists_from_library(include_unmonitored=True)
|
||||||
tasks["recently_imported"] = self._lidarr_repo.get_recently_imported(limit=15)
|
tasks["recently_imported"] = self._lidarr_repo.get_recently_imported(limit=15)
|
||||||
|
tasks["monitored_mbids"] = self._lidarr_repo.get_monitored_no_files_mbids()
|
||||||
|
|
||||||
if resolved_source == "listenbrainz" and lb_enabled and username:
|
if resolved_source == "listenbrainz" and lb_enabled and username:
|
||||||
lb_settings = self._preferences.get_listenbrainz_connection()
|
lb_settings = self._preferences.get_listenbrainz_connection()
|
||||||
@@ -195,6 +196,7 @@ class HomeService:
|
|||||||
library_album_mbids = {
|
library_album_mbids = {
|
||||||
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
(a.musicbrainz_id or "").lower() for a in library_albums if a.musicbrainz_id
|
||||||
}
|
}
|
||||||
|
monitored_mbids: set[str] = results.get("monitored_mbids") or set()
|
||||||
|
|
||||||
response = HomeResponse(integration_status=integration_status)
|
response = HomeResponse(integration_status=integration_status)
|
||||||
|
|
||||||
@@ -207,10 +209,10 @@ class HomeService:
|
|||||||
results, library_artist_mbids
|
results, library_artist_mbids
|
||||||
)
|
)
|
||||||
response.popular_albums = self._builders.build_popular_albums_section(
|
response.popular_albums = self._builders.build_popular_albums_section(
|
||||||
results, library_album_mbids
|
results, library_album_mbids, monitored_mbids
|
||||||
)
|
)
|
||||||
response.your_top_albums = self._builders.build_lb_user_top_albums_section(
|
response.your_top_albums = self._builders.build_lb_user_top_albums_section(
|
||||||
results, library_album_mbids
|
results, library_album_mbids, monitored_mbids
|
||||||
)
|
)
|
||||||
response.recently_played = self._builders.build_listenbrainz_recent_section(results)
|
response.recently_played = self._builders.build_listenbrainz_recent_section(results)
|
||||||
response.favorite_artists = self._builders.build_listenbrainz_favorites_section(results)
|
response.favorite_artists = self._builders.build_listenbrainz_favorites_section(results)
|
||||||
@@ -220,7 +222,7 @@ class HomeService:
|
|||||||
results, library_artist_mbids
|
results, library_artist_mbids
|
||||||
)
|
)
|
||||||
response.your_top_albums = self._builders.build_lastfm_top_albums_section(
|
response.your_top_albums = self._builders.build_lastfm_top_albums_section(
|
||||||
results, library_album_mbids
|
results, library_album_mbids, monitored_mbids
|
||||||
)
|
)
|
||||||
response.recently_played = self._builders.build_lastfm_recent_section(results)
|
response.recently_played = self._builders.build_lastfm_recent_section(results)
|
||||||
response.favorite_artists = self._builders.build_lastfm_favorites_section(results)
|
response.favorite_artists = self._builders.build_lastfm_favorites_section(results)
|
||||||
|
|||||||
@@ -60,24 +60,24 @@ class HomeSectionBuilders:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def build_popular_albums_section(
|
def build_popular_albums_section(
|
||||||
self, results: dict[str, Any], library_mbids: set[str]
|
self, results: dict[str, Any], library_mbids: set[str], monitored_mbids: set[str] | None = None
|
||||||
) -> HomeSection:
|
) -> HomeSection:
|
||||||
albums = results.get("lb_trending_albums") or []
|
albums = results.get("lb_trending_albums") or []
|
||||||
return HomeSection(
|
return HomeSection(
|
||||||
title="Popular Right Now",
|
title="Popular Right Now",
|
||||||
type="albums",
|
type="albums",
|
||||||
items=[self._transformers.lb_release_to_home(a, library_mbids) for a in albums[:15]],
|
items=[self._transformers.lb_release_to_home(a, library_mbids, monitored_mbids) for a in albums[:15]],
|
||||||
source="listenbrainz" if albums else None,
|
source="listenbrainz" if albums else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def build_lb_user_top_albums_section(
|
def build_lb_user_top_albums_section(
|
||||||
self, results: dict[str, Any], library_mbids: set[str]
|
self, results: dict[str, Any], library_mbids: set[str], monitored_mbids: set[str] | None = None
|
||||||
) -> HomeSection | None:
|
) -> HomeSection | None:
|
||||||
release_groups = results.get("lb_user_top_rgs") or []
|
release_groups = results.get("lb_user_top_rgs") or []
|
||||||
if not release_groups:
|
if not release_groups:
|
||||||
return None
|
return None
|
||||||
items = [
|
items = [
|
||||||
self._transformers.lb_release_to_home(rg, library_mbids)
|
self._transformers.lb_release_to_home(rg, library_mbids, monitored_mbids)
|
||||||
for rg in release_groups[:15]
|
for rg in release_groups[:15]
|
||||||
]
|
]
|
||||||
return HomeSection(
|
return HomeSection(
|
||||||
@@ -95,7 +95,8 @@ class HomeSectionBuilders:
|
|||||||
return HomeSection(title="Browse by Genre", type="genres", items=genres, source=source)
|
return HomeSection(title="Browse by Genre", type="genres", items=genres, source=source)
|
||||||
|
|
||||||
def build_fresh_releases_section(
|
def build_fresh_releases_section(
|
||||||
self, results: dict[str, Any], library_mbids: set[str]
|
self, results: dict[str, Any], library_mbids: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> HomeSection | None:
|
) -> HomeSection | None:
|
||||||
releases = results.get("lb_fresh")
|
releases = results.get("lb_fresh")
|
||||||
if not releases:
|
if not releases:
|
||||||
@@ -103,7 +104,7 @@ class HomeSectionBuilders:
|
|||||||
return HomeSection(
|
return HomeSection(
|
||||||
title="New From Artists You Follow",
|
title="New From Artists You Follow",
|
||||||
type="albums",
|
type="albums",
|
||||||
items=[self._transformers.lb_release_to_home(r, library_mbids) for r in releases[:15]],
|
items=[self._transformers.lb_release_to_home(r, library_mbids, monitored_mbids) for r in releases[:15]],
|
||||||
source="listenbrainz",
|
source="listenbrainz",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -170,12 +171,12 @@ class HomeSectionBuilders:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def build_lastfm_top_albums_section(
|
def build_lastfm_top_albums_section(
|
||||||
self, results: dict[str, Any], library_mbids: set[str]
|
self, results: dict[str, Any], library_mbids: set[str], monitored_mbids: set[str] | None = None
|
||||||
) -> HomeSection:
|
) -> HomeSection:
|
||||||
albums = results.get("lfm_top_albums") or []
|
albums = results.get("lfm_top_albums") or []
|
||||||
items = [
|
items = [
|
||||||
a for a in (
|
a for a in (
|
||||||
self._transformers.lastfm_album_to_home(album, library_mbids)
|
self._transformers.lastfm_album_to_home(album, library_mbids, monitored_mbids)
|
||||||
for album in albums[:15]
|
for album in albums[:15]
|
||||||
)
|
)
|
||||||
if a is not None
|
if a is not None
|
||||||
|
|||||||
@@ -70,9 +70,15 @@ class HomeDataTransformers:
|
|||||||
def lb_release_to_home(
|
def lb_release_to_home(
|
||||||
self,
|
self,
|
||||||
release: ListenBrainzReleaseGroup,
|
release: ListenBrainzReleaseGroup,
|
||||||
library_mbids: set[str]
|
library_mbids: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> HomeAlbum:
|
) -> HomeAlbum:
|
||||||
artist_mbid = release.artist_mbids[0] if release.artist_mbids else None
|
artist_mbid = release.artist_mbids[0] if release.artist_mbids else None
|
||||||
|
mbid_lower = (release.release_group_mbid or "").lower()
|
||||||
|
in_library = mbid_lower in library_mbids
|
||||||
|
monitored = (
|
||||||
|
not in_library and bool(monitored_mbids) and mbid_lower in monitored_mbids
|
||||||
|
)
|
||||||
return HomeAlbum(
|
return HomeAlbum(
|
||||||
mbid=release.release_group_mbid,
|
mbid=release.release_group_mbid,
|
||||||
name=release.release_group_name,
|
name=release.release_group_name,
|
||||||
@@ -81,7 +87,8 @@ class HomeDataTransformers:
|
|||||||
image_url=None,
|
image_url=None,
|
||||||
release_date=None,
|
release_date=None,
|
||||||
listen_count=release.listen_count,
|
listen_count=release.listen_count,
|
||||||
in_library=(release.release_group_mbid or "").lower() in library_mbids,
|
in_library=in_library,
|
||||||
|
monitored=monitored,
|
||||||
)
|
)
|
||||||
|
|
||||||
def jf_item_to_artist(
|
def jf_item_to_artist(
|
||||||
@@ -130,7 +137,13 @@ class HomeDataTransformers:
|
|||||||
self,
|
self,
|
||||||
album: LastFmAlbum,
|
album: LastFmAlbum,
|
||||||
library_mbids: set[str],
|
library_mbids: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> HomeAlbum | None:
|
) -> HomeAlbum | None:
|
||||||
|
mbid_lower = album.mbid.lower() if album.mbid else ""
|
||||||
|
in_library = mbid_lower in library_mbids if mbid_lower else False
|
||||||
|
monitored = (
|
||||||
|
not in_library and bool(monitored_mbids) and mbid_lower in monitored_mbids
|
||||||
|
) if mbid_lower else False
|
||||||
return HomeAlbum(
|
return HomeAlbum(
|
||||||
mbid=None,
|
mbid=None,
|
||||||
name=album.name,
|
name=album.name,
|
||||||
@@ -138,7 +151,8 @@ class HomeDataTransformers:
|
|||||||
artist_mbid=None,
|
artist_mbid=None,
|
||||||
image_url=album.image_url or None,
|
image_url=album.image_url or None,
|
||||||
listen_count=album.playcount,
|
listen_count=album.playcount,
|
||||||
in_library=album.mbid.lower() in library_mbids if album.mbid else False,
|
in_library=in_library,
|
||||||
|
monitored=monitored,
|
||||||
source="lastfm",
|
source="lastfm",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -159,14 +173,21 @@ class HomeDataTransformers:
|
|||||||
self,
|
self,
|
||||||
track: LastFmRecentTrack,
|
track: LastFmRecentTrack,
|
||||||
library_mbids: set[str],
|
library_mbids: set[str],
|
||||||
|
monitored_mbids: set[str] | None = None,
|
||||||
) -> HomeAlbum | None:
|
) -> HomeAlbum | None:
|
||||||
|
mbid_lower = (track.album_mbid or "").lower()
|
||||||
|
in_library = mbid_lower in library_mbids if track.album_mbid else False
|
||||||
|
monitored = (
|
||||||
|
not in_library and bool(monitored_mbids) and bool(mbid_lower) and mbid_lower in monitored_mbids
|
||||||
|
)
|
||||||
return HomeAlbum(
|
return HomeAlbum(
|
||||||
mbid=track.album_mbid,
|
mbid=track.album_mbid,
|
||||||
name=track.album_name or track.track_name,
|
name=track.album_name or track.track_name,
|
||||||
artist_name=track.artist_name,
|
artist_name=track.artist_name,
|
||||||
artist_mbid=track.artist_mbid,
|
artist_mbid=track.artist_mbid,
|
||||||
image_url=track.image_url or None,
|
image_url=track.image_url or None,
|
||||||
in_library=track.album_mbid.lower() in library_mbids if track.album_mbid else False,
|
in_library=in_library,
|
||||||
|
monitored=monitored,
|
||||||
source="lastfm",
|
source="lastfm",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,15 @@ class LibraryService:
|
|||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.error(f"Failed to fetch requested mbids: {e}")
|
logger.error(f"Failed to fetch requested mbids: {e}")
|
||||||
raise ExternalServiceError(f"Failed to fetch requested mbids: {e}")
|
raise ExternalServiceError(f"Failed to fetch requested mbids: {e}")
|
||||||
|
|
||||||
|
async def get_monitored_mbids(self) -> list[str]:
|
||||||
|
if not self._lidarr_repo.is_configured():
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return list(await self._lidarr_repo.get_monitored_no_files_mbids())
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.error(f"Failed to fetch monitored mbids: {e}")
|
||||||
|
raise ExternalServiceError(f"Failed to fetch monitored mbids: {e}")
|
||||||
|
|
||||||
async def get_artists(self, limit: int | None = None) -> list[LibraryArtist]:
|
async def get_artists(self, limit: int | None = None) -> list[LibraryArtist]:
|
||||||
try:
|
try:
|
||||||
@@ -348,8 +357,8 @@ class LibraryService:
|
|||||||
|
|
||||||
sync_succeeded = False
|
sync_succeeded = False
|
||||||
try:
|
try:
|
||||||
albums = await self._lidarr_repo.get_library()
|
albums = await self._lidarr_repo.get_library(include_unmonitored=True)
|
||||||
artists = await self._lidarr_repo.get_artists_from_library()
|
artists = await self._lidarr_repo.get_artists_from_library(include_unmonitored=True)
|
||||||
|
|
||||||
albums_data = [
|
albums_data = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class AlbumPhase:
|
|||||||
async def precache_album_data(
|
async def precache_album_data(
|
||||||
self,
|
self,
|
||||||
release_group_ids: list[str],
|
release_group_ids: list[str],
|
||||||
monitored_mbids: set[str],
|
library_mbids: set[str],
|
||||||
status_service: CacheStatusService,
|
status_service: CacheStatusService,
|
||||||
library_album_mbids: dict[str, Any] = None,
|
library_album_mbids: dict[str, Any] = None,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
@@ -48,11 +48,11 @@ class AlbumPhase:
|
|||||||
cached_info = await album_service._cache.get(cache_key)
|
cached_info = await album_service._cache.get(cache_key)
|
||||||
if not cached_info:
|
if not cached_info:
|
||||||
await status_service.update_progress(index + 1, f"Fetching metadata for {rgid[:8]}...", processed_albums=offset + index + 1, generation=generation)
|
await status_service.update_progress(index + 1, f"Fetching metadata for {rgid[:8]}...", processed_albums=offset + index + 1, generation=generation)
|
||||||
await album_service.get_album_info(rgid, monitored_mbids=monitored_mbids)
|
await album_service.get_album_info(rgid, library_mbids=library_mbids)
|
||||||
metadata_fetched = True
|
metadata_fetched = True
|
||||||
else:
|
else:
|
||||||
await status_service.update_progress(index + 1, f"Cached: {rgid[:8]}...", processed_albums=offset + index + 1, generation=generation)
|
await status_service.update_progress(index + 1, f"Cached: {rgid[:8]}...", processed_albums=offset + index + 1, generation=generation)
|
||||||
if rgid.lower() in monitored_mbids:
|
if rgid.lower() in library_mbids:
|
||||||
cache_filename = get_cache_filename(f"rg_{rgid}", "500")
|
cache_filename = get_cache_filename(f"rg_{rgid}", "500")
|
||||||
file_path = self._cover_repo.cache_dir / f"{cache_filename}.bin"
|
file_path = self._cover_repo.cache_dir / f"{cache_filename}.bin"
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
|
|||||||
@@ -210,12 +210,12 @@ class LibraryPrecacheService:
|
|||||||
if status_service.is_cancelled():
|
if status_service.is_cancelled():
|
||||||
return
|
return
|
||||||
|
|
||||||
monitored_mbids: set[str] = set()
|
library_album_mbids_set: set[str] = set()
|
||||||
for a in albums:
|
for a in albums:
|
||||||
mbid = getattr(a, 'musicbrainz_id', None) if hasattr(a, 'musicbrainz_id') else a.get('mbid') if isinstance(a, dict) else None
|
mbid = getattr(a, 'musicbrainz_id', None) if hasattr(a, 'musicbrainz_id') else a.get('mbid') if isinstance(a, dict) else None
|
||||||
if not is_unknown_mbid(mbid):
|
if not is_unknown_mbid(mbid):
|
||||||
monitored_mbids.add(mbid.lower())
|
library_album_mbids_set.add(mbid.lower())
|
||||||
deduped_release_groups = list(monitored_mbids)
|
deduped_release_groups = list(library_album_mbids_set)
|
||||||
if status_service.is_cancelled():
|
if status_service.is_cancelled():
|
||||||
return
|
return
|
||||||
items_needing_metadata = []
|
items_needing_metadata = []
|
||||||
@@ -234,7 +234,7 @@ class LibraryPrecacheService:
|
|||||||
for rgid in deduped_release_groups:
|
for rgid in deduped_release_groups:
|
||||||
if rgid in processed_albums:
|
if rgid in processed_albums:
|
||||||
continue
|
continue
|
||||||
if rgid.lower() in monitored_mbids:
|
if rgid.lower() in library_album_mbids_set:
|
||||||
cache_filename = get_cache_filename(f"rg_{rgid}", "500")
|
cache_filename = get_cache_filename(f"rg_{rgid}", "500")
|
||||||
file_path = self._cover_repo.cache_dir / f"{cache_filename}.bin"
|
file_path = self._cover_repo.cache_dir / f"{cache_filename}.bin"
|
||||||
cover_paths.append((rgid, file_path))
|
cover_paths.append((rgid, file_path))
|
||||||
@@ -245,7 +245,7 @@ class LibraryPrecacheService:
|
|||||||
already_cached = len(deduped_release_groups) - len(items_to_process) - len(processed_albums)
|
already_cached = len(deduped_release_groups) - len(items_to_process) - len(processed_albums)
|
||||||
if items_to_process:
|
if items_to_process:
|
||||||
await status_service.update_phase('albums', len(items_to_process), generation=generation)
|
await status_service.update_phase('albums', len(items_to_process), generation=generation)
|
||||||
await self._album_phase.precache_album_data(items_to_process, monitored_mbids, status_service, library_album_mbids, len(processed_albums), generation=generation)
|
await self._album_phase.precache_album_data(items_to_process, library_album_mbids_set, status_service, library_album_mbids, len(processed_albums), generation=generation)
|
||||||
else:
|
else:
|
||||||
await status_service.skip_phase('albums', generation=generation)
|
await status_service.skip_phase('albums', generation=generation)
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class SearchService:
|
|||||||
limits["albums"] = limit_albums
|
limits["albums"] = limit_albums
|
||||||
|
|
||||||
try:
|
try:
|
||||||
grouped, library_mbids_raw, queue_items_raw = await self._safe_gather(
|
grouped, library_mbids_raw, queue_items_raw, monitored_mbids_raw = await self._safe_gather(
|
||||||
self._mb_repo.search_grouped(
|
self._mb_repo.search_grouped(
|
||||||
query,
|
query,
|
||||||
limits=limits,
|
limits=limits,
|
||||||
@@ -173,10 +173,11 @@ class SearchService:
|
|||||||
),
|
),
|
||||||
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
||||||
self._lidarr_repo.get_queue(),
|
self._lidarr_repo.get_queue(),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
)
|
)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.error(f"Search gather failed unexpectedly: {e}")
|
logger.error(f"Search gather failed unexpectedly: {e}")
|
||||||
grouped, library_mbids_raw, queue_items_raw = None, None, None
|
grouped, library_mbids_raw, queue_items_raw, monitored_mbids_raw = None, None, None, None
|
||||||
|
|
||||||
if grouped is None:
|
if grouped is None:
|
||||||
logger.warning("MusicBrainz search returned no results or failed")
|
logger.warning("MusicBrainz search returned no results or failed")
|
||||||
@@ -188,10 +189,13 @@ class SearchService:
|
|||||||
else:
|
else:
|
||||||
queued_mbids = set()
|
queued_mbids = set()
|
||||||
|
|
||||||
|
monitored_mbids = monitored_mbids_raw or set()
|
||||||
|
|
||||||
for item in grouped.get("albums", []):
|
for item in grouped.get("albums", []):
|
||||||
mbid_lower = (item.musicbrainz_id or "").lower()
|
mbid_lower = (item.musicbrainz_id or "").lower()
|
||||||
item.in_library = mbid_lower in library_mbids
|
item.in_library = mbid_lower in library_mbids
|
||||||
item.requested = mbid_lower in queued_mbids and not item.in_library
|
item.requested = mbid_lower in queued_mbids and not item.in_library
|
||||||
|
item.monitored = mbid_lower in monitored_mbids and not item.in_library and not item.requested
|
||||||
|
|
||||||
all_results = grouped.get("artists", []) + grouped.get("albums", [])
|
all_results = grouped.get("artists", []) + grouped.get("albums", [])
|
||||||
await self._apply_audiodb_search_overlay(all_results)
|
await self._apply_audiodb_search_overlay(all_results)
|
||||||
@@ -246,9 +250,10 @@ class SearchService:
|
|||||||
return [], None
|
return [], None
|
||||||
|
|
||||||
if bucket == "albums":
|
if bucket == "albums":
|
||||||
library_mbids_raw, queue_items_raw = await self._safe_gather(
|
library_mbids_raw, queue_items_raw, monitored_mbids_raw = await self._safe_gather(
|
||||||
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
||||||
self._lidarr_repo.get_queue(),
|
self._lidarr_repo.get_queue(),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
)
|
)
|
||||||
library_mbids = library_mbids_raw or set()
|
library_mbids = library_mbids_raw or set()
|
||||||
if queue_items_raw:
|
if queue_items_raw:
|
||||||
@@ -256,10 +261,13 @@ class SearchService:
|
|||||||
else:
|
else:
|
||||||
queued_mbids = set()
|
queued_mbids = set()
|
||||||
|
|
||||||
|
monitored_mbids = monitored_mbids_raw or set()
|
||||||
|
|
||||||
for item in results:
|
for item in results:
|
||||||
mbid_lower = (item.musicbrainz_id or "").lower()
|
mbid_lower = (item.musicbrainz_id or "").lower()
|
||||||
item.in_library = mbid_lower in library_mbids
|
item.in_library = mbid_lower in library_mbids
|
||||||
item.requested = mbid_lower in queued_mbids and not item.in_library
|
item.requested = mbid_lower in queued_mbids and not item.in_library
|
||||||
|
item.monitored = mbid_lower in monitored_mbids and not item.in_library and not item.requested
|
||||||
|
|
||||||
await self._apply_audiodb_search_overlay(results)
|
await self._apply_audiodb_search_overlay(results)
|
||||||
|
|
||||||
@@ -288,9 +296,10 @@ class SearchService:
|
|||||||
|
|
||||||
grouped = grouped or {"artists": [], "albums": []}
|
grouped = grouped or {"artists": [], "albums": []}
|
||||||
|
|
||||||
library_mbids_raw, queue_items_raw = await self._safe_gather(
|
library_mbids_raw, queue_items_raw, monitored_mbids_raw = await self._safe_gather(
|
||||||
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
||||||
self._lidarr_repo.get_queue(),
|
self._lidarr_repo.get_queue(),
|
||||||
|
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||||
)
|
)
|
||||||
library_mbids = library_mbids_raw or set()
|
library_mbids = library_mbids_raw or set()
|
||||||
if queue_items_raw:
|
if queue_items_raw:
|
||||||
@@ -298,10 +307,13 @@ class SearchService:
|
|||||||
else:
|
else:
|
||||||
queued_mbids = set()
|
queued_mbids = set()
|
||||||
|
|
||||||
|
monitored_mbids = monitored_mbids_raw or set()
|
||||||
|
|
||||||
for item in grouped.get("albums", []):
|
for item in grouped.get("albums", []):
|
||||||
mbid_lower = (item.musicbrainz_id or "").lower()
|
mbid_lower = (item.musicbrainz_id or "").lower()
|
||||||
item.in_library = mbid_lower in library_mbids
|
item.in_library = mbid_lower in library_mbids
|
||||||
item.requested = mbid_lower in queued_mbids and not item.in_library
|
item.requested = mbid_lower in queued_mbids and not item.in_library
|
||||||
|
item.monitored = mbid_lower in monitored_mbids and not item.in_library and not item.requested
|
||||||
|
|
||||||
suggestions: list[SuggestResult] = []
|
suggestions: list[SuggestResult] = []
|
||||||
for item in grouped.get("artists", []) + grouped.get("albums", []):
|
for item in grouped.get("artists", []) + grouped.get("albums", []):
|
||||||
@@ -313,6 +325,7 @@ class SearchService:
|
|||||||
musicbrainz_id=item.musicbrainz_id,
|
musicbrainz_id=item.musicbrainz_id,
|
||||||
in_library=item.in_library,
|
in_library=item.in_library,
|
||||||
requested=item.requested,
|
requested=item.requested,
|
||||||
|
monitored=item.monitored,
|
||||||
disambiguation=item.disambiguation,
|
disambiguation=item.disambiguation,
|
||||||
score=item.score,
|
score=item.score,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ from infrastructure.cache.disk_cache import DiskMetadataCache
|
|||||||
from repositories.audiodb_models import AudioDBArtistImages, AudioDBAlbumImages
|
from repositories.audiodb_models import AudioDBArtistImages, AudioDBAlbumImages
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_hash(identifier: str) -> str:
|
||||||
|
return hashlib.sha1(f"{DiskMetadataCache._CACHE_VERSION}:{identifier}".encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_set_album_serializes_msgspec_struct_as_mapping(tmp_path):
|
async def test_set_album_serializes_msgspec_struct_as_mapping(tmp_path):
|
||||||
cache = DiskMetadataCache(base_path=tmp_path)
|
cache = DiskMetadataCache(base_path=tmp_path)
|
||||||
@@ -22,7 +26,7 @@ async def test_set_album_serializes_msgspec_struct_as_mapping(tmp_path):
|
|||||||
|
|
||||||
await cache.set_album(mbid, album_info, is_monitored=True)
|
await cache.set_album(mbid, album_info, is_monitored=True)
|
||||||
|
|
||||||
cache_hash = hashlib.sha1(mbid.encode()).hexdigest()
|
cache_hash = _cache_hash(mbid)
|
||||||
cache_file = tmp_path / "persistent" / "albums" / f"{cache_hash}.json"
|
cache_file = tmp_path / "persistent" / "albums" / f"{cache_hash}.json"
|
||||||
payload = json.loads(cache_file.read_text())
|
payload = json.loads(cache_file.read_text())
|
||||||
|
|
||||||
@@ -39,7 +43,7 @@ async def test_get_album_deletes_corrupt_string_payload(tmp_path):
|
|||||||
cache = DiskMetadataCache(base_path=tmp_path)
|
cache = DiskMetadataCache(base_path=tmp_path)
|
||||||
mbid = "8e1e9e51-38dc-4df3-8027-a0ada37d4674"
|
mbid = "8e1e9e51-38dc-4df3-8027-a0ada37d4674"
|
||||||
|
|
||||||
cache_hash = hashlib.sha1(mbid.encode()).hexdigest()
|
cache_hash = _cache_hash(mbid)
|
||||||
cache_file = tmp_path / "persistent" / "albums" / f"{cache_hash}.json"
|
cache_file = tmp_path / "persistent" / "albums" / f"{cache_hash}.json"
|
||||||
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
cache_file.write_text(json.dumps("AlbumInfo(title='Corrupt')"))
|
cache_file.write_text(json.dumps("AlbumInfo(title='Corrupt')"))
|
||||||
@@ -69,7 +73,7 @@ async def test_audiodb_artist_entity_routing(tmp_path):
|
|||||||
assert result["fanart_url"] == "https://example.com/fanart.jpg"
|
assert result["fanart_url"] == "https://example.com/fanart.jpg"
|
||||||
assert result["lookup_source"] == "mbid"
|
assert result["lookup_source"] == "mbid"
|
||||||
|
|
||||||
cache_hash = hashlib.sha1(mbid.encode()).hexdigest()
|
cache_hash = _cache_hash(mbid)
|
||||||
data_file = tmp_path / "recent" / "audiodb_artists" / f"{cache_hash}.json"
|
data_file = tmp_path / "recent" / "audiodb_artists" / f"{cache_hash}.json"
|
||||||
assert data_file.exists()
|
assert data_file.exists()
|
||||||
|
|
||||||
@@ -93,7 +97,7 @@ async def test_audiodb_album_entity_routing(tmp_path):
|
|||||||
assert result["album_back_url"] == "https://example.com/album_back.jpg"
|
assert result["album_back_url"] == "https://example.com/album_back.jpg"
|
||||||
assert result["lookup_source"] == "name"
|
assert result["lookup_source"] == "name"
|
||||||
|
|
||||||
cache_hash = hashlib.sha1(mbid.encode()).hexdigest()
|
cache_hash = _cache_hash(mbid)
|
||||||
persistent_file = tmp_path / "persistent" / "audiodb_albums" / f"{cache_hash}.json"
|
persistent_file = tmp_path / "persistent" / "audiodb_albums" / f"{cache_hash}.json"
|
||||||
assert persistent_file.exists()
|
assert persistent_file.exists()
|
||||||
|
|
||||||
@@ -160,7 +164,7 @@ async def test_audiodb_monitored_persistent_vs_recent(tmp_path):
|
|||||||
|
|
||||||
await cache._set_entity("audiodb_artist", mbid, images, is_monitored=True, ttl_seconds=None)
|
await cache._set_entity("audiodb_artist", mbid, images, is_monitored=True, ttl_seconds=None)
|
||||||
|
|
||||||
cache_hash = hashlib.sha1(mbid.encode()).hexdigest()
|
cache_hash = _cache_hash(mbid)
|
||||||
persistent_file = tmp_path / "persistent" / "audiodb_artists" / f"{cache_hash}.json"
|
persistent_file = tmp_path / "persistent" / "audiodb_artists" / f"{cache_hash}.json"
|
||||||
recent_file = tmp_path / "recent" / "audiodb_artists" / f"{cache_hash}.json"
|
recent_file = tmp_path / "recent" / "audiodb_artists" / f"{cache_hash}.json"
|
||||||
assert persistent_file.exists()
|
assert persistent_file.exists()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ def _sample_album_data() -> list[dict]:
|
|||||||
"releaseDate": "2023-01-15",
|
"releaseDate": "2023-01-15",
|
||||||
"added": "2023-01-10T12:00:00Z",
|
"added": "2023-01-10T12:00:00Z",
|
||||||
"images": [],
|
"images": [],
|
||||||
|
"statistics": {"trackFileCount": 5},
|
||||||
"artist": {
|
"artist": {
|
||||||
"artistName": "Artist A",
|
"artistName": "Artist A",
|
||||||
"foreignArtistId": "artist-a-mbid",
|
"foreignArtistId": "artist-a-mbid",
|
||||||
@@ -39,6 +40,7 @@ def _sample_album_data() -> list[dict]:
|
|||||||
"releaseDate": "2024-06-01",
|
"releaseDate": "2024-06-01",
|
||||||
"added": "2024-06-01T08:00:00Z",
|
"added": "2024-06-01T08:00:00Z",
|
||||||
"images": [],
|
"images": [],
|
||||||
|
"statistics": {"trackFileCount": 8},
|
||||||
"artist": {
|
"artist": {
|
||||||
"artistName": "Artist B",
|
"artistName": "Artist B",
|
||||||
"foreignArtistId": "artist-b-mbid",
|
"foreignArtistId": "artist-b-mbid",
|
||||||
@@ -52,6 +54,7 @@ def _sample_album_data() -> list[dict]:
|
|||||||
"releaseDate": "2020-03-01",
|
"releaseDate": "2020-03-01",
|
||||||
"added": "2020-03-01T00:00:00Z",
|
"added": "2020-03-01T00:00:00Z",
|
||||||
"images": [],
|
"images": [],
|
||||||
|
"statistics": {"trackFileCount": 3},
|
||||||
"artist": {
|
"artist": {
|
||||||
"artistName": "Artist C",
|
"artistName": "Artist C",
|
||||||
"foreignArtistId": "artist-c-mbid",
|
"foreignArtistId": "artist-c-mbid",
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ async def test_get_album_basic_info_does_not_use_library_cache_when_lidarr_paylo
|
|||||||
|
|
||||||
result = await service.get_album_basic_info("8e1e9e51-38dc-4df3-8027-a0ada37d4674")
|
result = await service.get_album_basic_info("8e1e9e51-38dc-4df3-8027-a0ada37d4674")
|
||||||
|
|
||||||
assert result.in_library is False
|
assert result.in_library is True
|
||||||
library_db.get_album_by_mbid.assert_not_awaited()
|
library_db.get_album_by_mbid.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ async def test_get_album_tracks_info_preserves_disc_numbers_from_lidarr():
|
|||||||
service, lidarr_repo, _ = _make_service()
|
service, lidarr_repo, _ = _make_service()
|
||||||
service._get_cached_album_info = AsyncMock(return_value=None)
|
service._get_cached_album_info = AsyncMock(return_value=None)
|
||||||
lidarr_repo.is_configured.return_value = True
|
lidarr_repo.is_configured.return_value = True
|
||||||
lidarr_repo.get_album_details = AsyncMock(return_value={"id": 42, "monitored": True})
|
lidarr_repo.get_album_details = AsyncMock(return_value={"id": 42, "monitored": True, "statistics": {"trackFileCount": 1}})
|
||||||
lidarr_repo.get_album_tracks = AsyncMock(
|
lidarr_repo.get_album_tracks = AsyncMock(
|
||||||
return_value=[
|
return_value=[
|
||||||
{
|
{
|
||||||
@@ -135,7 +135,7 @@ async def test_get_album_tracks_info_multi_disc_same_track_numbers():
|
|||||||
service, lidarr_repo, _ = _make_service()
|
service, lidarr_repo, _ = _make_service()
|
||||||
service._get_cached_album_info = AsyncMock(return_value=None)
|
service._get_cached_album_info = AsyncMock(return_value=None)
|
||||||
lidarr_repo.is_configured.return_value = True
|
lidarr_repo.is_configured.return_value = True
|
||||||
lidarr_repo.get_album_details = AsyncMock(return_value={"id": 42, "monitored": True})
|
lidarr_repo.get_album_details = AsyncMock(return_value={"id": 42, "monitored": True, "statistics": {"trackFileCount": 1}})
|
||||||
lidarr_repo.get_album_tracks = AsyncMock(
|
lidarr_repo.get_album_tracks = AsyncMock(
|
||||||
return_value=[
|
return_value=[
|
||||||
{"track_number": 1, "disc_number": 1, "title": "Intro", "duration_ms": 1000},
|
{"track_number": 1, "disc_number": 1, "title": "Intro", "duration_ms": 1000},
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ def _make_service(*, cached_artist: ArtistInfo | None = None) -> tuple[ArtistSer
|
|||||||
lidarr_repo.is_configured.return_value = False
|
lidarr_repo.is_configured.return_value = False
|
||||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||||
lidarr_repo.get_requested_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_requested_mbids = AsyncMock(return_value=set())
|
||||||
|
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||||
lidarr_repo.get_artist_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_artist_mbids = AsyncMock(return_value=set())
|
||||||
|
|
||||||
wikidata_repo = AsyncMock()
|
wikidata_repo = AsyncMock()
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ def _make_service(
|
|||||||
lidarr_repo.get_artist_details = AsyncMock(return_value=lidarr_artist)
|
lidarr_repo.get_artist_details = AsyncMock(return_value=lidarr_artist)
|
||||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||||
lidarr_repo.get_requested_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_requested_mbids = AsyncMock(return_value=set())
|
||||||
|
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||||
lidarr_repo.get_artist_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_artist_mbids = AsyncMock(return_value=set())
|
||||||
|
|
||||||
wikidata_repo = AsyncMock()
|
wikidata_repo = AsyncMock()
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ def _search_service(audiodb: MagicMock | None = None) -> SearchService:
|
|||||||
lidarr_repo = MagicMock()
|
lidarr_repo = MagicMock()
|
||||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||||
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
||||||
|
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||||
coverart_repo = MagicMock()
|
coverart_repo = MagicMock()
|
||||||
prefs = MagicMock()
|
prefs = MagicMock()
|
||||||
prefs.get_preferences.return_value = MagicMock(secondary_types=[])
|
prefs.get_preferences.return_value = MagicMock(secondary_types=[])
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ def _search_service(audiodb=None) -> SearchService:
|
|||||||
lidarr_repo = MagicMock()
|
lidarr_repo = MagicMock()
|
||||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||||
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
||||||
|
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||||
coverart_repo = MagicMock()
|
coverart_repo = MagicMock()
|
||||||
prefs = MagicMock()
|
prefs = MagicMock()
|
||||||
prefs.get_preferences.return_value = MagicMock(secondary_types=[])
|
prefs.get_preferences.return_value = MagicMock(secondary_types=[])
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ def _make_service(
|
|||||||
else:
|
else:
|
||||||
lidarr_repo.get_queue = AsyncMock(return_value=queue_items or [])
|
lidarr_repo.get_queue = AsyncMock(return_value=queue_items or [])
|
||||||
|
|
||||||
|
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||||
|
|
||||||
coverart_repo = MagicMock()
|
coverart_repo = MagicMock()
|
||||||
preferences_service = MagicMock()
|
preferences_service = MagicMock()
|
||||||
preferences_service.get_preferences.return_value = _make_preferences()
|
preferences_service.get_preferences.return_value = _make_preferences()
|
||||||
@@ -300,6 +302,7 @@ async def test_suggest_deduplication_single_mb_call():
|
|||||||
lidarr_repo = MagicMock()
|
lidarr_repo = MagicMock()
|
||||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||||
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
||||||
|
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||||
|
|
||||||
coverart_repo = MagicMock()
|
coverart_repo = MagicMock()
|
||||||
preferences_service = MagicMock()
|
preferences_service = MagicMock()
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ def _make_search_service(audiodb_service=None) -> SearchService:
|
|||||||
lidarr_repo = MagicMock()
|
lidarr_repo = MagicMock()
|
||||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||||
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
||||||
|
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||||
coverart_repo = MagicMock()
|
coverart_repo = MagicMock()
|
||||||
prefs = MagicMock()
|
prefs = MagicMock()
|
||||||
prefs.get_preferences.return_value = MagicMock(secondary_types=[])
|
prefs.get_preferences.return_value = MagicMock(secondary_types=[])
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class TestCooldownOnlyOnSuccess:
|
|||||||
async def test_retry_after_failed_sync_is_not_cooldown_blocked(self):
|
async def test_retry_after_failed_sync_is_not_cooldown_blocked(self):
|
||||||
call_count = 0
|
call_count = 0
|
||||||
|
|
||||||
async def fail_then_succeed():
|
async def fail_then_succeed(**kwargs):
|
||||||
nonlocal call_count
|
nonlocal call_count
|
||||||
call_count += 1
|
call_count += 1
|
||||||
if call_count == 1:
|
if call_count == 1:
|
||||||
@@ -94,7 +94,7 @@ class TestSyncFutureDedup:
|
|||||||
call_count = 0
|
call_count = 0
|
||||||
sync_event = asyncio.Event()
|
sync_event = asyncio.Event()
|
||||||
|
|
||||||
async def slow_get_library():
|
async def slow_get_library(**kwargs):
|
||||||
nonlocal call_count
|
nonlocal call_count
|
||||||
call_count += 1
|
call_count += 1
|
||||||
sync_event.set()
|
sync_event.set()
|
||||||
@@ -118,7 +118,7 @@ class TestSyncFutureDedup:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_concurrent_sync_failure_propagates_to_waiter(self):
|
async def test_concurrent_sync_failure_propagates_to_waiter(self):
|
||||||
"""When the producer fails, deduped waiters get the real exception."""
|
"""When the producer fails, deduped waiters get the real exception."""
|
||||||
async def failing_get_library():
|
async def failing_get_library(**kwargs):
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
raise RuntimeError("Lidarr DNS failure")
|
raise RuntimeError("Lidarr DNS failure")
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import { libraryStore } from '$lib/stores/library';
|
import { libraryStore } from '$lib/stores/library';
|
||||||
import { integrationStore } from '$lib/stores/integration';
|
import { integrationStore } from '$lib/stores/integration';
|
||||||
import { requestAlbum } from '$lib/utils/albumRequest';
|
import { requestAlbum } from '$lib/utils/albumRequest';
|
||||||
|
import { toggleAlbumMonitored } from '$lib/utils/monitorAlbum';
|
||||||
|
import { toastStore } from '$lib/stores/toast';
|
||||||
import { formatListenCount } from '$lib/utils/formatting';
|
import { formatListenCount } from '$lib/utils/formatting';
|
||||||
import { getListenTitle } from '$lib/utils/enrichment';
|
import { getListenTitle } from '$lib/utils/enrichment';
|
||||||
import { Download, Music2 } from 'lucide-svelte';
|
import { Download, Music2 } from 'lucide-svelte';
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
let listenTitle = $derived(getListenTitle(enrichmentSource, 'album'));
|
let listenTitle = $derived(getListenTitle(enrichmentSource, 'album'));
|
||||||
|
|
||||||
let requesting = $state(false);
|
let requesting = $state(false);
|
||||||
|
let monitoredLoading = $state(false);
|
||||||
|
|
||||||
let inLibrary = $derived(
|
let inLibrary = $derived(
|
||||||
libraryStore.isInLibrary(album.musicbrainz_id) || album.in_library || false
|
libraryStore.isInLibrary(album.musicbrainz_id) || album.in_library || false
|
||||||
@@ -38,6 +41,11 @@
|
|||||||
!album.in_library &&
|
!album.in_library &&
|
||||||
(album.requested || libraryStore.isRequested(album.musicbrainz_id))
|
(album.requested || libraryStore.isRequested(album.musicbrainz_id))
|
||||||
);
|
);
|
||||||
|
let isMonitored = $derived(
|
||||||
|
!inLibrary &&
|
||||||
|
!isRequested &&
|
||||||
|
(album.monitored || libraryStore.isMonitored(album.musicbrainz_id))
|
||||||
|
);
|
||||||
|
|
||||||
async function handleRequest(e: Event) {
|
async function handleRequest(e: Event) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -56,9 +64,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleToggleMonitored() {
|
||||||
|
monitoredLoading = true;
|
||||||
|
try {
|
||||||
|
await toggleAlbumMonitored(album.musicbrainz_id, false);
|
||||||
|
album.monitored = false;
|
||||||
|
album = album;
|
||||||
|
} catch {
|
||||||
|
toastStore.show({ message: 'Failed to update monitoring status', type: 'error' });
|
||||||
|
} finally {
|
||||||
|
monitoredLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleDeleted() {
|
function handleDeleted() {
|
||||||
album.in_library = false;
|
album.in_library = false;
|
||||||
album.requested = false;
|
album.requested = false;
|
||||||
|
album.monitored = false;
|
||||||
album = album;
|
album = album;
|
||||||
onremoved?.();
|
onremoved?.();
|
||||||
}
|
}
|
||||||
@@ -161,6 +183,15 @@
|
|||||||
artistName={album.artist || 'Unknown'}
|
artistName={album.artist || 'Unknown'}
|
||||||
ondeleted={handleDeleted}
|
ondeleted={handleDeleted}
|
||||||
/>
|
/>
|
||||||
|
{:else if isMonitored}
|
||||||
|
<LibraryBadge
|
||||||
|
status="monitored"
|
||||||
|
musicbrainzId={album.musicbrainz_id}
|
||||||
|
albumTitle={album.title}
|
||||||
|
artistName={album.artist || 'Unknown'}
|
||||||
|
ontogglemonitored={handleToggleMonitored}
|
||||||
|
{monitoredLoading}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
musicbrainz_id: da.musicbrainz_id,
|
musicbrainz_id: da.musicbrainz_id,
|
||||||
in_library: da.in_library,
|
in_library: da.in_library,
|
||||||
requested: da.requested,
|
requested: da.requested,
|
||||||
|
monitored: da.monitored,
|
||||||
cover_url: da.cover_url
|
cover_url: da.cover_url
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check } from 'lucide-svelte';
|
import { Check, Bookmark } from 'lucide-svelte';
|
||||||
import AlbumImage from '$lib/components/AlbumImage.svelte';
|
import AlbumImage from '$lib/components/AlbumImage.svelte';
|
||||||
import AlbumCardOverlay from '$lib/components/AlbumCardOverlay.svelte';
|
import AlbumCardOverlay from '$lib/components/AlbumCardOverlay.svelte';
|
||||||
import type { HomeAlbum } from '$lib/types';
|
import type { HomeAlbum } from '$lib/types';
|
||||||
@@ -40,6 +40,10 @@
|
|||||||
<div class="absolute top-2 left-2 z-20 badge badge-success badge-sm gap-1 opacity-90">
|
<div class="absolute top-2 left-2 z-20 badge badge-success badge-sm gap-1 opacity-90">
|
||||||
<Check class="w-3 h-3" />
|
<Check class="w-3 h-3" />
|
||||||
</div>
|
</div>
|
||||||
|
{:else if album.monitored && !album.requested}
|
||||||
|
<div class="absolute top-2 left-2 z-20 badge badge-neutral badge-sm gap-1 opacity-90">
|
||||||
|
<Bookmark class="w-3 h-3" />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if album.mbid && album.in_library}
|
{#if album.mbid && album.in_library}
|
||||||
<AlbumCardOverlay
|
<AlbumCardOverlay
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
X,
|
X,
|
||||||
Check,
|
Check,
|
||||||
|
Bookmark,
|
||||||
Music2,
|
Music2,
|
||||||
Tv,
|
Tv,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
@@ -184,7 +185,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if item.in_library}
|
{#if item.in_library}
|
||||||
<div class="absolute top-2 right-2 badge badge-success badge-sm">
|
<div class="absolute top-2 right-2 badge badge-success badge-sm">
|
||||||
<Check class="w-3 h-3" />
|
<Check class="w-3.5 h-3.5" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</figure>
|
</figure>
|
||||||
@@ -217,7 +218,11 @@
|
|||||||
/>
|
/>
|
||||||
{#if item.in_library}
|
{#if item.in_library}
|
||||||
<div class="absolute top-2 left-2 z-20 badge badge-success badge-sm">
|
<div class="absolute top-2 left-2 z-20 badge badge-success badge-sm">
|
||||||
<Check class="w-3 h-3" />
|
<Check class="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
{:else if item.monitored && !item.requested}
|
||||||
|
<div class="absolute top-2 left-2 z-20 badge badge-neutral badge-sm">
|
||||||
|
<Bookmark class="w-3.5 h-3.5" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.mbid && item.in_library}
|
{#if item.mbid && item.in_library}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
musicbrainz_id: rg.id,
|
musicbrainz_id: rg.id,
|
||||||
in_library: libraryStore.isInLibrary(rg.id) || rg.in_library,
|
in_library: libraryStore.isInLibrary(rg.id) || rg.in_library,
|
||||||
requested: rg.requested,
|
requested: rg.requested,
|
||||||
|
monitored: rg.monitored,
|
||||||
cover_url: null,
|
cover_url: null,
|
||||||
type_info: rg.type
|
type_info: rg.type
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check, Clock, Trash2 } from 'lucide-svelte';
|
import { Check, Clock, Trash2, Bookmark, BookmarkX } from 'lucide-svelte';
|
||||||
import { colors } from '$lib/colors';
|
import { colors } from '$lib/colors';
|
||||||
import { STATUS_COLORS } from '$lib/constants';
|
import { STATUS_COLORS } from '$lib/constants';
|
||||||
import DeleteAlbumModal from './DeleteAlbumModal.svelte';
|
import DeleteAlbumModal from './DeleteAlbumModal.svelte';
|
||||||
import ArtistRemovedModal from './ArtistRemovedModal.svelte';
|
import ArtistRemovedModal from './ArtistRemovedModal.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
status: 'library' | 'requested';
|
status: 'library' | 'requested' | 'monitored';
|
||||||
musicbrainzId: string;
|
musicbrainzId: string;
|
||||||
albumTitle: string;
|
albumTitle: string;
|
||||||
artistName: string;
|
artistName: string;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
positioning?: string;
|
positioning?: string;
|
||||||
ondeleted?: (result: { artist_removed: boolean; artist_name?: string | null }) => void;
|
ondeleted?: (result: { artist_removed: boolean; artist_name?: string | null }) => void;
|
||||||
|
ontogglemonitored?: () => void;
|
||||||
|
monitoredLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -22,7 +24,9 @@
|
|||||||
artistName,
|
artistName,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
positioning = '',
|
positioning = '',
|
||||||
ondeleted
|
ondeleted,
|
||||||
|
ontogglemonitored,
|
||||||
|
monitoredLoading = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let showDeleteModal = $state(false);
|
let showDeleteModal = $state(false);
|
||||||
@@ -31,11 +35,11 @@
|
|||||||
|
|
||||||
const sizeClasses = $derived(
|
const sizeClasses = $derived(
|
||||||
{
|
{
|
||||||
sm: { button: 'p-0.5', icon: 'w-2.5 h-2.5', strokeWidth: status === 'library' ? '3' : '2' },
|
sm: { button: 'p-1', icon: 'w-3.5 h-3.5', strokeWidth: status === 'library' ? '3' : '2' },
|
||||||
md: { button: 'p-1.5', icon: 'h-4 w-4', strokeWidth: status === 'library' ? '3' : '2' },
|
md: { button: 'p-1.5', icon: 'h-5 w-5', strokeWidth: status === 'library' ? '3' : '2' },
|
||||||
lg: {
|
lg: {
|
||||||
button: 'p-0',
|
button: 'p-0',
|
||||||
icon: 'h-4 w-4 sm:h-5 sm:w-5',
|
icon: 'h-5 w-5 sm:h-6 sm:w-6',
|
||||||
strokeWidth: status === 'library' ? '3' : '2'
|
strokeWidth: status === 'library' ? '3' : '2'
|
||||||
}
|
}
|
||||||
}[size]
|
}[size]
|
||||||
@@ -44,11 +48,21 @@
|
|||||||
const lgButtonClass = $derived(
|
const lgButtonClass = $derived(
|
||||||
size === 'lg' ? 'w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center' : ''
|
size === 'lg' ? 'w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center' : ''
|
||||||
);
|
);
|
||||||
const bgColor = $derived(status === 'library' ? colors.accent : STATUS_COLORS.REQUESTED);
|
const bgColor = $derived(
|
||||||
|
status === 'library'
|
||||||
|
? colors.accent
|
||||||
|
: status === 'requested'
|
||||||
|
? STATUS_COLORS.REQUESTED
|
||||||
|
: STATUS_COLORS.MONITORED
|
||||||
|
);
|
||||||
|
|
||||||
function handleClick(e: Event) {
|
function handleClick(e: Event) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (status === 'monitored') {
|
||||||
|
ontogglemonitored?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
showDeleteModal = true;
|
showDeleteModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,31 +80,62 @@
|
|||||||
class="{positioning} rounded-full shadow-sm transition-colors duration-200 group/badge {sizeClasses.button} {lgButtonClass}"
|
class="{positioning} rounded-full shadow-sm transition-colors duration-200 group/badge {sizeClasses.button} {lgButtonClass}"
|
||||||
style="background-color: {bgColor};"
|
style="background-color: {bgColor};"
|
||||||
onclick={handleClick}
|
onclick={handleClick}
|
||||||
onmouseenter={(e) => {
|
onmouseenter={status === 'monitored'
|
||||||
e.currentTarget.style.backgroundColor = '#ef4444';
|
? (e) => {
|
||||||
}}
|
e.currentTarget.style.filter = 'brightness(1.3)';
|
||||||
|
}
|
||||||
|
: (e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '#ef4444';
|
||||||
|
}}
|
||||||
onmouseleave={(e) => {
|
onmouseleave={(e) => {
|
||||||
e.currentTarget.style.backgroundColor = bgColor;
|
e.currentTarget.style.backgroundColor = bgColor;
|
||||||
|
e.currentTarget.style.filter = '';
|
||||||
}}
|
}}
|
||||||
aria-label={status === 'library' ? 'Remove from library' : 'Remove request'}
|
aria-label={status === 'library'
|
||||||
|
? 'Remove from library'
|
||||||
|
: status === 'requested'
|
||||||
|
? 'Remove request'
|
||||||
|
: 'Toggle monitoring'}
|
||||||
|
disabled={monitoredLoading}
|
||||||
>
|
>
|
||||||
{#if status === 'library'}
|
{#if monitoredLoading}
|
||||||
|
<span class="loading loading-spinner {sizeClasses.icon}" style="color: {colors.secondary};"
|
||||||
|
></span>
|
||||||
|
{:else if status === 'library'}
|
||||||
<Check
|
<Check
|
||||||
class="{sizeClasses.icon} group-hover/badge:hidden"
|
class="{sizeClasses.icon} group-hover/badge:hidden"
|
||||||
color={colors.secondary}
|
color={colors.secondary}
|
||||||
strokeWidth={Number(sizeClasses.strokeWidth)}
|
strokeWidth={Number(sizeClasses.strokeWidth)}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else if status === 'requested'}
|
||||||
<Clock
|
<Clock
|
||||||
class="{sizeClasses.icon} group-hover/badge:hidden"
|
class="{sizeClasses.icon} group-hover/badge:hidden"
|
||||||
color={colors.secondary}
|
color={colors.secondary}
|
||||||
strokeWidth={Number(sizeClasses.strokeWidth)}
|
strokeWidth={Number(sizeClasses.strokeWidth)}
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<Bookmark
|
||||||
|
class="{sizeClasses.icon} group-hover/badge:hidden"
|
||||||
|
color={colors.secondary}
|
||||||
|
strokeWidth={Number(sizeClasses.strokeWidth)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if status === 'monitored' && !monitoredLoading}
|
||||||
|
<BookmarkX
|
||||||
|
class="{sizeClasses.icon} hidden group-hover/badge:block"
|
||||||
|
color={colors.secondary}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
{:else if status !== 'monitored'}
|
||||||
|
<Trash2
|
||||||
|
class="{sizeClasses.icon} hidden group-hover/badge:block"
|
||||||
|
color="white"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Trash2 class="{sizeClasses.icon} hidden group-hover/badge:block" color="white" strokeWidth={2} />
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if showDeleteModal}
|
{#if status !== 'monitored' && showDeleteModal}
|
||||||
<DeleteAlbumModal
|
<DeleteAlbumModal
|
||||||
{albumTitle}
|
{albumTitle}
|
||||||
{artistName}
|
{artistName}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { ChevronDown, Download } from 'lucide-svelte';
|
import { ChevronDown, Download } from 'lucide-svelte';
|
||||||
import { colors } from '$lib/colors';
|
import { colors } from '$lib/colors';
|
||||||
import { libraryStore } from '$lib/stores/library';
|
import { libraryStore } from '$lib/stores/library';
|
||||||
|
import { toggleAlbumMonitored } from '$lib/utils/monitorAlbum';
|
||||||
|
import { toastStore } from '$lib/stores/toast';
|
||||||
import AlbumImage from './AlbumImage.svelte';
|
import AlbumImage from './AlbumImage.svelte';
|
||||||
import LibraryBadge from './LibraryBadge.svelte';
|
import LibraryBadge from './LibraryBadge.svelte';
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
year?: number | null;
|
year?: number | null;
|
||||||
in_library?: boolean;
|
in_library?: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RemoveResult {
|
interface RemoveResult {
|
||||||
@@ -46,11 +49,31 @@
|
|||||||
function handleDeleted(rg: Release, result: RemoveResult) {
|
function handleDeleted(rg: Release, result: RemoveResult) {
|
||||||
rg.in_library = false;
|
rg.in_library = false;
|
||||||
rg.requested = false;
|
rg.requested = false;
|
||||||
|
rg.monitored = false;
|
||||||
releases = releases;
|
releases = releases;
|
||||||
onRemoved?.(result);
|
onRemoved?.(result);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet requestButton(rg: Release, ariaLabel: string)}
|
||||||
|
<button
|
||||||
|
class="w-8 h-8 sm:w-10 sm:h-10 rounded-full opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity duration-200 border-none flex items-center justify-center shadow-sm"
|
||||||
|
style="background-color: {colors.accent};"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRequest(rg.id, rg.title);
|
||||||
|
}}
|
||||||
|
disabled={requestingIds.has(rg.id)}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
{#if requestingIds.has(rg.id)}
|
||||||
|
<span class="loading loading-spinner loading-xs" style="color: {colors.secondary};"></span>
|
||||||
|
{:else}
|
||||||
|
<Download class="h-4 w-4 sm:h-5 sm:w-5" color={colors.secondary} strokeWidth={2.5} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="bg-base-300 rounded-t-box">
|
<div class="bg-base-300 rounded-t-box">
|
||||||
<button
|
<button
|
||||||
@@ -105,30 +128,31 @@
|
|||||||
size="lg"
|
size="lg"
|
||||||
ondeleted={(result) => handleDeleted(rg, result)}
|
ondeleted={(result) => handleDeleted(rg, result)}
|
||||||
/>
|
/>
|
||||||
|
{:else if !libraryStore.isInLibrary(rg.id) && !libraryStore.isRequested(rg.id) && (rg.monitored || libraryStore.isMonitored(rg.id))}
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
{@render requestButton(rg, `Request ${rg.title}`)}
|
||||||
|
<LibraryBadge
|
||||||
|
status="monitored"
|
||||||
|
musicbrainzId={rg.id}
|
||||||
|
albumTitle={rg.title}
|
||||||
|
{artistName}
|
||||||
|
size="lg"
|
||||||
|
ontogglemonitored={async () => {
|
||||||
|
try {
|
||||||
|
await toggleAlbumMonitored(rg.id, false);
|
||||||
|
rg.monitored = false;
|
||||||
|
releases = releases;
|
||||||
|
} catch {
|
||||||
|
toastStore.show({
|
||||||
|
message: 'Failed to update monitoring status',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
{@render requestButton(rg, `Request ${title.toLowerCase().slice(0, -1)}`)}
|
||||||
class="w-8 h-8 sm:w-10 sm:h-10 rounded-full opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity duration-200 border-none flex items-center justify-center shadow-sm"
|
|
||||||
style="background-color: {colors.accent};"
|
|
||||||
onclick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onRequest(rg.id, rg.title);
|
|
||||||
}}
|
|
||||||
disabled={requestingIds.has(rg.id)}
|
|
||||||
aria-label="Request {title.toLowerCase().slice(0, -1)}"
|
|
||||||
>
|
|
||||||
{#if requestingIds.has(rg.id)}
|
|
||||||
<span
|
|
||||||
class="loading loading-spinner loading-xs"
|
|
||||||
style="color: {colors.secondary};"
|
|
||||||
></span>
|
|
||||||
{:else}
|
|
||||||
<Download
|
|
||||||
class="h-4 w-4 sm:h-5 sm:w-5"
|
|
||||||
color={colors.secondary}
|
|
||||||
strokeWidth={2.5}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -271,6 +271,9 @@
|
|||||||
{#if result.requested}
|
{#if result.requested}
|
||||||
<span class="badge badge-sm badge-warning">Requested</span>
|
<span class="badge badge-sm badge-warning">Requested</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if result.monitored && !result.in_library && !result.requested}
|
||||||
|
<span class="badge badge-sm badge-neutral">Monitored</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check, Search } from 'lucide-svelte';
|
import { Check, Bookmark, Search } from 'lucide-svelte';
|
||||||
import type { HomeAlbum, HomeArtist } from '$lib/types';
|
import type { HomeAlbum, HomeArtist } from '$lib/types';
|
||||||
import { formatListenCount } from '$lib/utils/formatting';
|
import { formatListenCount } from '$lib/utils/formatting';
|
||||||
import AlbumImage from './AlbumImage.svelte';
|
import AlbumImage from './AlbumImage.svelte';
|
||||||
@@ -82,6 +82,11 @@
|
|||||||
<Check class="h-3 w-3" />
|
<Check class="h-3 w-3" />
|
||||||
In Library
|
In Library
|
||||||
</div>
|
</div>
|
||||||
|
{:else if item.monitored && !('requested' in item && item.requested)}
|
||||||
|
<div class="badge badge-neutral">
|
||||||
|
<Bookmark class="h-3 w-3" />
|
||||||
|
Monitored
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if isAlbum(item) && item.mbid && item.in_library}
|
{#if isAlbum(item) && item.mbid && item.in_library}
|
||||||
@@ -127,6 +132,10 @@
|
|||||||
<div class="badge badge-success badge-sm absolute left-1 top-1 z-20">
|
<div class="badge badge-success badge-sm absolute left-1 top-1 z-20">
|
||||||
<Check class="h-3 w-3" />
|
<Check class="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
|
{:else if item.monitored && !('requested' in item && item.requested)}
|
||||||
|
<div class="badge badge-neutral badge-sm absolute left-1 top-1 z-20">
|
||||||
|
<Bookmark class="h-3 w-3" />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.mbid && item.in_library}
|
{#if item.mbid && item.in_library}
|
||||||
<AlbumCardOverlay
|
<AlbumCardOverlay
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
import { colors } from '$lib/colors';
|
import { colors } from '$lib/colors';
|
||||||
import { libraryStore } from '$lib/stores/library';
|
import { libraryStore } from '$lib/stores/library';
|
||||||
import { requestAlbum } from '$lib/utils/albumRequest';
|
import { requestAlbum } from '$lib/utils/albumRequest';
|
||||||
|
import { toggleAlbumMonitored } from '$lib/utils/monitorAlbum';
|
||||||
|
import { toastStore } from '$lib/stores/toast';
|
||||||
import AlbumImage from './AlbumImage.svelte';
|
import AlbumImage from './AlbumImage.svelte';
|
||||||
import LibraryBadge from './LibraryBadge.svelte';
|
import LibraryBadge from './LibraryBadge.svelte';
|
||||||
import LastFmPlaceholder from './LastFmPlaceholder.svelte';
|
import LastFmPlaceholder from './LastFmPlaceholder.svelte';
|
||||||
@@ -24,12 +26,14 @@
|
|||||||
|
|
||||||
let libraryMbids = new SvelteSet<string>();
|
let libraryMbids = new SvelteSet<string>();
|
||||||
let requestedMbids = new SvelteSet<string>();
|
let requestedMbids = new SvelteSet<string>();
|
||||||
|
let monitoredMbids = new SvelteSet<string>();
|
||||||
let storeInitialized = $state(false);
|
let storeInitialized = $state(false);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const unsubscribe = libraryStore.subscribe((state) => {
|
const unsubscribe = libraryStore.subscribe((state) => {
|
||||||
libraryMbids = new SvelteSet(state.mbidSet);
|
libraryMbids = new SvelteSet(state.mbidSet);
|
||||||
requestedMbids = new SvelteSet(state.requestedSet);
|
requestedMbids = new SvelteSet(state.requestedSet);
|
||||||
|
monitoredMbids = new SvelteSet(state.monitoredSet);
|
||||||
storeInitialized = state.initialized;
|
storeInitialized = state.initialized;
|
||||||
});
|
});
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
@@ -48,6 +52,14 @@
|
|||||||
return requestedMbids.has(mbid) && !libraryMbids.has(mbid);
|
return requestedMbids.has(mbid) && !libraryMbids.has(mbid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMonitored(album: TopAlbum): boolean {
|
||||||
|
if (isInLibrary(album) || isRequested(album)) return false;
|
||||||
|
const mbid = album.release_group_mbid?.toLowerCase();
|
||||||
|
if (!mbid) return false;
|
||||||
|
if (storeInitialized) return monitoredMbids.has(mbid);
|
||||||
|
return album.monitored || monitoredMbids.has(mbid);
|
||||||
|
}
|
||||||
|
|
||||||
function isRequesting(album: TopAlbum): boolean {
|
function isRequesting(album: TopAlbum): boolean {
|
||||||
return album.release_group_mbid ? requestingIds.has(album.release_group_mbid) : false;
|
return album.release_group_mbid ? requestingIds.has(album.release_group_mbid) : false;
|
||||||
}
|
}
|
||||||
@@ -129,6 +141,26 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
positioning="absolute -bottom-1 -right-1"
|
positioning="absolute -bottom-1 -right-1"
|
||||||
/>
|
/>
|
||||||
|
{:else if isMonitored(album)}
|
||||||
|
<LibraryBadge
|
||||||
|
status="monitored"
|
||||||
|
musicbrainzId={album.release_group_mbid}
|
||||||
|
albumTitle={album.title}
|
||||||
|
artistName={album.artist_name || 'Unknown'}
|
||||||
|
size="sm"
|
||||||
|
positioning="absolute -bottom-1 -right-1"
|
||||||
|
ontogglemonitored={async () => {
|
||||||
|
if (!album.release_group_mbid) return;
|
||||||
|
try {
|
||||||
|
await toggleAlbumMonitored(album.release_group_mbid, false);
|
||||||
|
} catch {
|
||||||
|
toastStore.show({
|
||||||
|
message: 'Failed to update monitoring status',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -143,7 +175,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !isInLibrary(album) && !isRequested(album)}
|
{#if !isInLibrary(album) && !isRequested(album) && !isMonitored(album)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-circle btn-sm opacity-0 group-hover:opacity-100 transition-all shrink-0 hover:scale-110 hover:brightness-110"
|
class="btn btn-circle btn-sm opacity-0 group-hover:opacity-100 transition-all shrink-0 hover:scale-110 hover:brightness-110"
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ export const PLACEHOLDER_COLORS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const STATUS_COLORS = {
|
export const STATUS_COLORS = {
|
||||||
REQUESTED: '#F59E0B'
|
REQUESTED: '#F59E0B',
|
||||||
|
MONITORED: '#6B7280'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
export const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { api } from '$lib/api/client';
|
|||||||
export interface LibraryState {
|
export interface LibraryState {
|
||||||
mbidSet: Set<string>;
|
mbidSet: Set<string>;
|
||||||
requestedSet: Set<string>;
|
requestedSet: Set<string>;
|
||||||
|
monitoredSet: Set<string>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
lastUpdated: number | null;
|
lastUpdated: number | null;
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
@@ -14,6 +15,7 @@ export interface LibraryState {
|
|||||||
const initialState: LibraryState = {
|
const initialState: LibraryState = {
|
||||||
mbidSet: new Set(),
|
mbidSet: new Set(),
|
||||||
requestedSet: new Set(),
|
requestedSet: new Set(),
|
||||||
|
monitoredSet: new Set(),
|
||||||
loading: false,
|
loading: false,
|
||||||
lastUpdated: null,
|
lastUpdated: null,
|
||||||
initialized: false
|
initialized: false
|
||||||
@@ -22,6 +24,7 @@ const initialState: LibraryState = {
|
|||||||
type LibraryCacheData = {
|
type LibraryCacheData = {
|
||||||
mbids: string[];
|
mbids: string[];
|
||||||
requested: string[];
|
requested: string[];
|
||||||
|
monitored: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function createLibraryStore() {
|
function createLibraryStore() {
|
||||||
@@ -33,18 +36,24 @@ function createLibraryStore() {
|
|||||||
|
|
||||||
function normalizeCachedData(data: LibraryCacheData | string[]): LibraryCacheData {
|
function normalizeCachedData(data: LibraryCacheData | string[]): LibraryCacheData {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return { mbids: data, requested: [] };
|
return { mbids: data, requested: [], monitored: [] };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
mbids: data.mbids ?? [],
|
mbids: data.mbids ?? [],
|
||||||
requested: data.requested ?? []
|
requested: data.requested ?? [],
|
||||||
|
monitored: data.monitored ?? []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistState(mbidSet: Set<string>, requestedSet: Set<string>) {
|
function persistState(
|
||||||
|
mbidSet: Set<string>,
|
||||||
|
requestedSet: Set<string>,
|
||||||
|
monitoredSet: Set<string>
|
||||||
|
) {
|
||||||
cache.set({
|
cache.set({
|
||||||
mbids: [...mbidSet],
|
mbids: [...mbidSet],
|
||||||
requested: [...requestedSet]
|
requested: [...requestedSet],
|
||||||
|
monitored: [...monitoredSet]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +66,9 @@ function createLibraryStore() {
|
|||||||
const normalized = normalizeCachedData(cached.data);
|
const normalized = normalizeCachedData(cached.data);
|
||||||
const mbids = normalized.mbids.map((m) => m.toLowerCase());
|
const mbids = normalized.mbids.map((m) => m.toLowerCase());
|
||||||
const requested = normalized.requested.map((m) => m.toLowerCase());
|
const requested = normalized.requested.map((m) => m.toLowerCase());
|
||||||
|
const monitored = normalized.monitored.map((m) => m.toLowerCase());
|
||||||
|
|
||||||
if (mbids.length === 0 && requested.length === 0) {
|
if (mbids.length === 0 && requested.length === 0 && monitored.length === 0) {
|
||||||
await fetchLibraryMbids(false);
|
await fetchLibraryMbids(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -67,6 +77,7 @@ function createLibraryStore() {
|
|||||||
...s,
|
...s,
|
||||||
mbidSet: new Set(mbids),
|
mbidSet: new Set(mbids),
|
||||||
requestedSet: new Set(requested),
|
requestedSet: new Set(requested),
|
||||||
|
monitoredSet: new Set(monitored),
|
||||||
lastUpdated: cached.timestamp,
|
lastUpdated: cached.timestamp,
|
||||||
initialized: true
|
initialized: true
|
||||||
}));
|
}));
|
||||||
@@ -86,22 +97,26 @@ function createLibraryStore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.global.get<{ mbids?: string[]; requested_mbids?: string[] }>(
|
const data = await api.global.get<{
|
||||||
'/api/v1/library/mbids'
|
mbids?: string[];
|
||||||
);
|
requested_mbids?: string[];
|
||||||
|
monitored_mbids?: string[];
|
||||||
|
}>('/api/v1/library/mbids');
|
||||||
const mbids: string[] = (data.mbids || []).map((m: string) => m.toLowerCase());
|
const mbids: string[] = (data.mbids || []).map((m: string) => m.toLowerCase());
|
||||||
const requested: string[] = (data.requested_mbids || []).map((m: string) => m.toLowerCase());
|
const requested: string[] = (data.requested_mbids || []).map((m: string) => m.toLowerCase());
|
||||||
|
const monitored: string[] = (data.monitored_mbids || []).map((m: string) => m.toLowerCase());
|
||||||
|
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
mbidSet: new Set(mbids),
|
mbidSet: new Set(mbids),
|
||||||
requestedSet: new Set(requested),
|
requestedSet: new Set(requested),
|
||||||
|
monitoredSet: new Set(monitored),
|
||||||
loading: false,
|
loading: false,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
initialized: true
|
initialized: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
cache.set({ mbids, requested });
|
cache.set({ mbids, requested, monitored });
|
||||||
} catch {
|
} catch {
|
||||||
if (!background) {
|
if (!background) {
|
||||||
update((s) => ({ ...s, loading: false, initialized: true }));
|
update((s) => ({ ...s, loading: false, initialized: true }));
|
||||||
@@ -121,8 +136,10 @@ function createLibraryStore() {
|
|||||||
newSet.add(mbid.toLowerCase());
|
newSet.add(mbid.toLowerCase());
|
||||||
const newRequested = new Set(s.requestedSet);
|
const newRequested = new Set(s.requestedSet);
|
||||||
newRequested.delete(mbid.toLowerCase());
|
newRequested.delete(mbid.toLowerCase());
|
||||||
persistState(newSet, newRequested);
|
const newMonitored = new Set(s.monitoredSet);
|
||||||
return { ...s, mbidSet: newSet, requestedSet: newRequested };
|
newMonitored.delete(mbid.toLowerCase());
|
||||||
|
persistState(newSet, newRequested, newMonitored);
|
||||||
|
return { ...s, mbidSet: newSet, requestedSet: newRequested, monitoredSet: newMonitored };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,8 +149,10 @@ function createLibraryStore() {
|
|||||||
newSet.delete(mbid.toLowerCase());
|
newSet.delete(mbid.toLowerCase());
|
||||||
const newRequested = new Set(s.requestedSet);
|
const newRequested = new Set(s.requestedSet);
|
||||||
newRequested.delete(mbid.toLowerCase());
|
newRequested.delete(mbid.toLowerCase());
|
||||||
persistState(newSet, newRequested);
|
const newMonitored = new Set(s.monitoredSet);
|
||||||
return { ...s, mbidSet: newSet, requestedSet: newRequested };
|
newMonitored.delete(mbid.toLowerCase());
|
||||||
|
persistState(newSet, newRequested, newMonitored);
|
||||||
|
return { ...s, mbidSet: newSet, requestedSet: newRequested, monitoredSet: newMonitored };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,17 +161,44 @@ function createLibraryStore() {
|
|||||||
if (s.mbidSet.has(mbid.toLowerCase())) {
|
if (s.mbidSet.has(mbid.toLowerCase())) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
const newSet = new Set(s.requestedSet);
|
const lower = mbid.toLowerCase();
|
||||||
newSet.add(mbid.toLowerCase());
|
const newRequested = new Set(s.requestedSet);
|
||||||
persistState(s.mbidSet, newSet);
|
newRequested.add(lower);
|
||||||
return { ...s, requestedSet: newSet };
|
const newMonitored = new Set(s.monitoredSet);
|
||||||
|
newMonitored.delete(lower);
|
||||||
|
persistState(s.mbidSet, newRequested, newMonitored);
|
||||||
|
return { ...s, requestedSet: newRequested, monitoredSet: newMonitored };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRequested(mbid: string | null | undefined): boolean {
|
function isRequested(mbid: string | null | undefined): boolean {
|
||||||
if (!mbid) return false;
|
if (!mbid) return false;
|
||||||
|
const lower = mbid.toLowerCase();
|
||||||
const state = get({ subscribe });
|
const state = get({ subscribe });
|
||||||
return state.requestedSet.has(mbid.toLowerCase()) && !state.mbidSet.has(mbid.toLowerCase());
|
return state.requestedSet.has(lower) && !state.mbidSet.has(lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMonitored(mbid: string | null | undefined): boolean {
|
||||||
|
if (!mbid) return false;
|
||||||
|
const state = get({ subscribe });
|
||||||
|
return (
|
||||||
|
state.monitoredSet.has(mbid.toLowerCase()) &&
|
||||||
|
!state.mbidSet.has(mbid.toLowerCase()) &&
|
||||||
|
!state.requestedSet.has(mbid.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitored(mbid: string, monitored: boolean) {
|
||||||
|
update((s) => {
|
||||||
|
const newMonitored = new Set(s.monitoredSet);
|
||||||
|
if (monitored) {
|
||||||
|
newMonitored.add(mbid.toLowerCase());
|
||||||
|
} else {
|
||||||
|
newMonitored.delete(mbid.toLowerCase());
|
||||||
|
}
|
||||||
|
persistState(s.mbidSet, s.requestedSet, newMonitored);
|
||||||
|
return { ...s, monitoredSet: newMonitored };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
@@ -179,6 +225,8 @@ function createLibraryStore() {
|
|||||||
removeMbid,
|
removeMbid,
|
||||||
isRequested,
|
isRequested,
|
||||||
addRequested,
|
addRequested,
|
||||||
|
isMonitored,
|
||||||
|
setMonitored,
|
||||||
updateCacheTTL: cache.updateTTL
|
updateCacheTTL: cache.updateTTL
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export type Album = {
|
|||||||
musicbrainz_id: string;
|
musicbrainz_id: string;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
cover_url?: string | null;
|
cover_url?: string | null;
|
||||||
album_thumb_url?: string | null;
|
album_thumb_url?: string | null;
|
||||||
album_back_url?: string | null;
|
album_back_url?: string | null;
|
||||||
@@ -63,6 +64,7 @@ export type SuggestResult = {
|
|||||||
musicbrainz_id: string;
|
musicbrainz_id: string;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
disambiguation?: string | null;
|
disambiguation?: string | null;
|
||||||
score: number;
|
score: number;
|
||||||
};
|
};
|
||||||
@@ -111,6 +113,7 @@ export type ReleaseGroup = {
|
|||||||
first_release_date?: string;
|
first_release_date?: string;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExternalLink = {
|
export type ExternalLink = {
|
||||||
@@ -207,6 +210,7 @@ export type AlbumInfo = {
|
|||||||
total_length?: number | null;
|
total_length?: number | null;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
cover_url?: string | null;
|
cover_url?: string | null;
|
||||||
album_thumb_url?: string | null;
|
album_thumb_url?: string | null;
|
||||||
album_back_url?: string | null;
|
album_back_url?: string | null;
|
||||||
@@ -229,6 +233,7 @@ export type AlbumBasicInfo = {
|
|||||||
disambiguation?: string | null;
|
disambiguation?: string | null;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
cover_url?: string | null;
|
cover_url?: string | null;
|
||||||
album_thumb_url?: string | null;
|
album_thumb_url?: string | null;
|
||||||
};
|
};
|
||||||
@@ -274,6 +279,7 @@ export type HomeArtist = {
|
|||||||
image_url: string | null;
|
image_url: string | null;
|
||||||
listen_count: number | null;
|
listen_count: number | null;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HomeAlbum = {
|
export type HomeAlbum = {
|
||||||
@@ -286,6 +292,7 @@ export type HomeAlbum = {
|
|||||||
listen_count: number | null;
|
listen_count: number | null;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HomeTrack = {
|
export type HomeTrack = {
|
||||||
@@ -506,6 +513,7 @@ export type SimilarArtist = {
|
|||||||
name: string;
|
name: string;
|
||||||
listen_count: number;
|
listen_count: number;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
image_url?: string | null;
|
image_url?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -552,6 +560,7 @@ export type TopAlbum = {
|
|||||||
listen_count: number;
|
listen_count: number;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
cover_url?: string | null;
|
cover_url?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -569,6 +578,7 @@ export type DiscoveryAlbum = {
|
|||||||
year?: number | null;
|
year?: number | null;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
requested?: boolean;
|
requested?: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
cover_url?: string | null;
|
cover_url?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -587,6 +597,7 @@ export type DiscoverQueueItemLight = {
|
|||||||
recommendation_reason: string;
|
recommendation_reason: string;
|
||||||
is_wildcard: boolean;
|
is_wildcard: boolean;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiscoverQueueEnrichment = {
|
export type DiscoverQueueEnrichment = {
|
||||||
@@ -747,6 +758,7 @@ export type RequestHistoryItem = {
|
|||||||
completed_at?: string | null;
|
completed_at?: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
in_library: boolean;
|
in_library: boolean;
|
||||||
|
monitored?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActiveRequestsResponse = {
|
export type ActiveRequestsResponse = {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { libraryStore } from '$lib/stores/library';
|
||||||
|
import { api } from '$lib/api/client';
|
||||||
|
|
||||||
|
export async function toggleAlbumMonitored(mbid: string, monitored: boolean): Promise<void> {
|
||||||
|
await api.global.put(`/api/v1/albums/${mbid}/monitor`, { monitored });
|
||||||
|
libraryStore.setMonitored(mbid, monitored);
|
||||||
|
}
|
||||||
@@ -59,15 +59,19 @@
|
|||||||
loadingTracks={state.loadingTracks}
|
loadingTracks={state.loadingTracks}
|
||||||
inLibrary={state.inLibrary}
|
inLibrary={state.inLibrary}
|
||||||
isRequested={state.isRequested}
|
isRequested={state.isRequested}
|
||||||
|
albumMonitored={state.albumMonitored}
|
||||||
requesting={state.requesting}
|
requesting={state.requesting}
|
||||||
refreshing={state.refreshing}
|
refreshing={state.refreshing}
|
||||||
pollingForSources={state.pollingForSources}
|
pollingForSources={state.pollingForSources}
|
||||||
lidarrConfigured={$integrationStore.lidarr}
|
lidarrConfigured={$integrationStore.lidarr}
|
||||||
|
monitorToggleLoading={state.monitorToggleLoading}
|
||||||
artistMonitored={state.artistMonitored}
|
artistMonitored={state.artistMonitored}
|
||||||
|
artistInLidarr={state.artistInLidarr}
|
||||||
onrequest={state.handleRequest}
|
onrequest={state.handleRequest}
|
||||||
ondelete={state.handleDeleteClick}
|
ondelete={state.handleDeleteClick}
|
||||||
onrefresh={state.refreshAll}
|
onrefresh={state.refreshAll}
|
||||||
onartistclick={state.goToArtist}
|
onartistclick={state.goToArtist}
|
||||||
|
ontogglemonitored={state.handleToggleMonitored}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if state.loadingTracks}
|
{#if state.loadingTracks}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import AlbumImage from '$lib/components/AlbumImage.svelte';
|
import AlbumImage from '$lib/components/AlbumImage.svelte';
|
||||||
import HeroBackdrop from '$lib/components/HeroBackdrop.svelte';
|
import HeroBackdrop from '$lib/components/HeroBackdrop.svelte';
|
||||||
import { formatTotalDuration } from '$lib/utils/formatting';
|
import { formatTotalDuration } from '$lib/utils/formatting';
|
||||||
import { Check, Trash2, Clock, Plus, RefreshCw } from 'lucide-svelte';
|
import { Check, Trash2, Clock, Plus, RefreshCw, Bookmark } from 'lucide-svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
album: AlbumBasicInfo;
|
album: AlbumBasicInfo;
|
||||||
@@ -13,16 +13,20 @@
|
|||||||
loadingTracks: boolean;
|
loadingTracks: boolean;
|
||||||
inLibrary: boolean;
|
inLibrary: boolean;
|
||||||
isRequested: boolean;
|
isRequested: boolean;
|
||||||
|
albumMonitored: boolean;
|
||||||
requesting: boolean;
|
requesting: boolean;
|
||||||
refreshing: boolean;
|
refreshing: boolean;
|
||||||
pollingForSources: boolean;
|
pollingForSources: boolean;
|
||||||
lidarrConfigured: boolean;
|
lidarrConfigured: boolean;
|
||||||
|
monitorToggleLoading?: boolean;
|
||||||
|
|
||||||
artistMonitored?: boolean;
|
artistMonitored?: boolean;
|
||||||
|
artistInLidarr?: boolean;
|
||||||
onrequest: (opts?: { monitorArtist?: boolean; autoDownloadArtist?: boolean }) => void;
|
onrequest: (opts?: { monitorArtist?: boolean; autoDownloadArtist?: boolean }) => void;
|
||||||
ondelete: () => void;
|
ondelete: () => void;
|
||||||
onrefresh: () => void;
|
onrefresh: () => void;
|
||||||
onartistclick: () => void;
|
onartistclick: () => void;
|
||||||
|
ontogglemonitored: (monitored: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -31,15 +35,19 @@
|
|||||||
loadingTracks,
|
loadingTracks,
|
||||||
inLibrary,
|
inLibrary,
|
||||||
isRequested,
|
isRequested,
|
||||||
|
albumMonitored,
|
||||||
requesting,
|
requesting,
|
||||||
refreshing,
|
refreshing,
|
||||||
pollingForSources,
|
pollingForSources,
|
||||||
lidarrConfigured,
|
lidarrConfigured,
|
||||||
|
monitorToggleLoading = false,
|
||||||
artistMonitored = false,
|
artistMonitored = false,
|
||||||
|
artistInLidarr = false,
|
||||||
onrequest,
|
onrequest,
|
||||||
ondelete,
|
ondelete,
|
||||||
onrefresh,
|
onrefresh,
|
||||||
onartistclick
|
onartistclick,
|
||||||
|
ontogglemonitored
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let monitorArtist = $state(false);
|
let monitorArtist = $state(false);
|
||||||
@@ -59,6 +67,8 @@
|
|||||||
? getApiUrl(`/api/v1/covers/release-group/${album.musicbrainz_id}?size=250`)
|
? getApiUrl(`/api/v1/covers/release-group/${album.musicbrainz_id}?size=250`)
|
||||||
: null)
|
: null)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showMonitorToggle = $derived(lidarrConfigured && artistInLidarr);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="album-hero group relative overflow-hidden rounded-2xl transition-all duration-500">
|
<div class="album-hero group relative overflow-hidden rounded-2xl transition-all duration-500">
|
||||||
@@ -72,7 +82,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="relative z-10 flex flex-col lg:flex-row gap-6 lg:gap-8 p-4 sm:p-6 lg:p-8">
|
<div class="relative z-10 flex flex-col lg:flex-row gap-6 lg:gap-8 p-4 sm:p-6 lg:p-8">
|
||||||
{#if (inLibrary || isRequested) && lidarrConfigured}
|
{#if (inLibrary || isRequested || albumMonitored) && lidarrConfigured}
|
||||||
<button
|
<button
|
||||||
class="absolute top-3 right-3 btn btn-sm btn-ghost btn-circle z-20"
|
class="absolute top-3 right-3 btn btn-sm btn-ghost btn-circle z-20"
|
||||||
onclick={onrefresh}
|
onclick={onrefresh}
|
||||||
@@ -223,6 +233,23 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showMonitorToggle}
|
||||||
|
<label class="flex items-center gap-2 pt-2 cursor-pointer">
|
||||||
|
<Bookmark class="h-4 w-4 text-base-content/70" />
|
||||||
|
<span class="text-sm text-base-content/70">Monitor</span>
|
||||||
|
{#if monitorToggleLoading}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={albumMonitored}
|
||||||
|
onchange={() => ontogglemonitored(!albumMonitored)}
|
||||||
|
class="toggle toggle-sm toggle-accent"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { artistHref } from '$lib/utils/entityRoutes';
|
|||||||
import type { AlbumBasicInfo, YouTubeTrackLink, YouTubeLink, YouTubeQuotaStatus } from '$lib/types';
|
import type { AlbumBasicInfo, YouTubeTrackLink, YouTubeLink, YouTubeQuotaStatus } from '$lib/types';
|
||||||
import { compareDiscTrack, getDiscTrackKey } from '$lib/player/queueHelpers';
|
import { compareDiscTrack, getDiscTrackKey } from '$lib/player/queueHelpers';
|
||||||
import { requestAlbum } from '$lib/utils/albumRequest';
|
import { requestAlbum } from '$lib/utils/albumRequest';
|
||||||
|
import { toggleAlbumMonitored } from '$lib/utils/monitorAlbum';
|
||||||
|
|
||||||
export interface EventHandlerDeps {
|
export interface EventHandlerDeps {
|
||||||
getAlbum: () => AlbumBasicInfo | null;
|
getAlbum: () => AlbumBasicInfo | null;
|
||||||
@@ -18,6 +19,7 @@ export interface EventHandlerDeps {
|
|||||||
setShowDeleteModal: (v: boolean) => void;
|
setShowDeleteModal: (v: boolean) => void;
|
||||||
setShowArtistRemovedModal: (v: boolean) => void;
|
setShowArtistRemovedModal: (v: boolean) => void;
|
||||||
setRemovedArtistName: (v: string) => void;
|
setRemovedArtistName: (v: string) => void;
|
||||||
|
setMonitorToggleLoading: (v: boolean) => void;
|
||||||
setToast: (msg: string, type: 'success' | 'error' | 'info' | 'warning') => void;
|
setToast: (msg: string, type: 'success' | 'error' | 'info' | 'warning') => void;
|
||||||
setShowToast: (v: boolean) => void;
|
setShowToast: (v: boolean) => void;
|
||||||
onRequestSuccess?: (opts?: { monitorArtist?: boolean; autoDownloadArtist?: boolean }) => void;
|
onRequestSuccess?: (opts?: { monitorArtist?: boolean; autoDownloadArtist?: boolean }) => void;
|
||||||
@@ -85,6 +87,7 @@ export function createEventHandlers(deps: EventHandlerDeps) {
|
|||||||
if (album) {
|
if (album) {
|
||||||
album.in_library = false;
|
album.in_library = false;
|
||||||
album.requested = false;
|
album.requested = false;
|
||||||
|
album.monitored = false;
|
||||||
deps.setAlbum(album);
|
deps.setAlbum(album);
|
||||||
deps.albumBasicCacheSet(album, deps.getAlbumId());
|
deps.albumBasicCacheSet(album, deps.getAlbumId());
|
||||||
}
|
}
|
||||||
@@ -96,6 +99,26 @@ export function createEventHandlers(deps: EventHandlerDeps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleToggleMonitored(monitored: boolean): Promise<void> {
|
||||||
|
const album = deps.getAlbum();
|
||||||
|
if (!album) return;
|
||||||
|
deps.setMonitorToggleLoading(true);
|
||||||
|
try {
|
||||||
|
await toggleAlbumMonitored(album.musicbrainz_id, monitored);
|
||||||
|
const current = deps.getAlbum();
|
||||||
|
if (current) {
|
||||||
|
current.monitored = monitored;
|
||||||
|
deps.setAlbum(current);
|
||||||
|
deps.albumBasicCacheSet(current, deps.getAlbumId());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
deps.setToast('Failed to update monitoring status', 'error');
|
||||||
|
deps.setShowToast(true);
|
||||||
|
} finally {
|
||||||
|
deps.setMonitorToggleLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function goToArtist(): void {
|
function goToArtist(): void {
|
||||||
const album = deps.getAlbum();
|
const album = deps.getAlbum();
|
||||||
// eslint-disable-next-line svelte/no-navigation-without-resolve -- artistHref uses resolve() internally
|
// eslint-disable-next-line svelte/no-navigation-without-resolve -- artistHref uses resolve() internally
|
||||||
@@ -110,6 +133,7 @@ export function createEventHandlers(deps: EventHandlerDeps) {
|
|||||||
handleRequest,
|
handleRequest,
|
||||||
handleDeleteClick,
|
handleDeleteClick,
|
||||||
handleDeleted,
|
handleDeleted,
|
||||||
|
handleToggleMonitored,
|
||||||
goToArtist
|
goToArtist
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||||||
let playlistModalRef = $state<{ open: (tracks: QueueItem[]) => void } | null>(null);
|
let playlistModalRef = $state<{ open: (tracks: QueueItem[]) => void } | null>(null);
|
||||||
let abortController: AbortController | null = null;
|
let abortController: AbortController | null = null;
|
||||||
let refreshing = $state(false);
|
let refreshing = $state(false);
|
||||||
|
let monitorToggleLoading = $state(false);
|
||||||
let pollingForSources = $state(false);
|
let pollingForSources = $state(false);
|
||||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
let artistInLidarr = $state(false);
|
let artistInLidarr = $state(false);
|
||||||
@@ -130,6 +131,17 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||||||
const isRequested = $derived(
|
const isRequested = $derived(
|
||||||
!!(album && !inLibrary && (album.requested || libraryStore.isRequested(album.musicbrainz_id)))
|
!!(album && !inLibrary && (album.requested || libraryStore.isRequested(album.musicbrainz_id)))
|
||||||
);
|
);
|
||||||
|
const isMonitored = $derived(
|
||||||
|
!!(
|
||||||
|
album &&
|
||||||
|
!inLibrary &&
|
||||||
|
!isRequested &&
|
||||||
|
(album.monitored || libraryStore.isMonitored(album.musicbrainz_id))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const albumMonitored = $derived(
|
||||||
|
!!(album && (album.monitored || libraryStore.isMonitored(album.musicbrainz_id)))
|
||||||
|
);
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
if (abortController) {
|
if (abortController) {
|
||||||
@@ -562,6 +574,7 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||||||
setShowDeleteModal: (v) => (showDeleteModal = v),
|
setShowDeleteModal: (v) => (showDeleteModal = v),
|
||||||
setShowArtistRemovedModal: (v) => (showArtistRemovedModal = v),
|
setShowArtistRemovedModal: (v) => (showArtistRemovedModal = v),
|
||||||
setRemovedArtistName: (v) => (removedArtistName = v),
|
setRemovedArtistName: (v) => (removedArtistName = v),
|
||||||
|
setMonitorToggleLoading: (v) => (monitorToggleLoading = v),
|
||||||
setToast: (msg, type) => {
|
setToast: (msg, type) => {
|
||||||
toastMessage = msg;
|
toastMessage = msg;
|
||||||
toastType = type;
|
toastType = type;
|
||||||
@@ -800,6 +813,12 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||||||
get isRequested() {
|
get isRequested() {
|
||||||
return isRequested;
|
return isRequested;
|
||||||
},
|
},
|
||||||
|
get isMonitored() {
|
||||||
|
return isMonitored;
|
||||||
|
},
|
||||||
|
get albumMonitored() {
|
||||||
|
return albumMonitored;
|
||||||
|
},
|
||||||
get artistInLidarr() {
|
get artistInLidarr() {
|
||||||
return artistInLidarr;
|
return artistInLidarr;
|
||||||
},
|
},
|
||||||
@@ -809,6 +828,9 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||||||
get refreshing() {
|
get refreshing() {
|
||||||
return refreshing;
|
return refreshing;
|
||||||
},
|
},
|
||||||
|
get monitorToggleLoading() {
|
||||||
|
return monitorToggleLoading;
|
||||||
|
},
|
||||||
get pollingForSources() {
|
get pollingForSources() {
|
||||||
return pollingForSources;
|
return pollingForSources;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user