mirror of
https://github.com/HabiRabbu/Musicseerr
synced 2026-04-21 13:37:27 +00:00
In library rework + Monitored/Unmonitored statuses (#50)
* In library rework + Monitored/Unmonitored statuses * address comments + format
This commit is contained in:
parent
6ca23bc725
commit
d24e26fb32
59 changed files with 772 additions and 187 deletions
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Optional
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request, status
|
||||
from core.exceptions import ClientDisconnectedError
|
||||
|
|
@ -11,9 +11,12 @@ from services.album_enrichment_service import AlbumEnrichmentService
|
|||
from services.navidrome_library_service import NavidromeLibraryService
|
||||
from infrastructure.validators import is_unknown_mbid
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
async def get_album_lastfm_enrichment(
|
||||
album_id: str,
|
||||
|
|
|
|||
|
|
@ -112,11 +112,12 @@ async def get_library_stats(
|
|||
async def get_library_mbids(
|
||||
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_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)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class AlbumBasicInfo(AppStruct):
|
|||
disambiguation: str | None = None
|
||||
in_library: bool = False
|
||||
requested: bool = False
|
||||
monitored: bool = False
|
||||
cover_url: str | None = None
|
||||
album_thumb_url: str | None = None
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class DiscoverQueueItemLight(AppStruct):
|
|||
cover_url: str | None = None
|
||||
is_wildcard: bool = False
|
||||
in_library: bool = False
|
||||
monitored: bool = False
|
||||
|
||||
|
||||
class DiscoverQueueEnrichment(AppStruct):
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class TopAlbum(AppStruct):
|
|||
listen_count: int = 0
|
||||
in_library: bool = False
|
||||
requested: bool = False
|
||||
monitored: bool = False
|
||||
cover_url: str | None = None
|
||||
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ class DiscoveryAlbum(AppStruct):
|
|||
year: int | None = None
|
||||
in_library: bool = False
|
||||
requested: bool = False
|
||||
monitored: bool = False
|
||||
cover_url: str | None = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class HomeAlbum(AppStruct):
|
|||
listen_count: int | None = None
|
||||
in_library: bool = False
|
||||
requested: bool = False
|
||||
monitored: bool = False
|
||||
source: str | None = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ class SyncLibraryResponse(AppStruct):
|
|||
class LibraryMbidsResponse(AppStruct):
|
||||
mbids: list[str] = []
|
||||
requested_mbids: list[str] = []
|
||||
monitored_mbids: list[str] = []
|
||||
|
||||
|
||||
class LibraryGroupedResponse(AppStruct):
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class SuggestResult(AppStruct):
|
|||
year: int | None = None
|
||||
in_library: bool = False
|
||||
requested: bool = False
|
||||
monitored: bool = False
|
||||
disambiguation: str | None = None
|
||||
score: int = 0
|
||||
|
||||
|
|
|
|||
4
backend/infrastructure/cache/cache_keys.py
vendored
4
backend/infrastructure/cache/cache_keys.py
vendored
|
|
@ -154,6 +154,10 @@ def lidarr_requested_mbids_key() -> str:
|
|||
return f"{LIDARR_REQUESTED_PREFIX}_mbids"
|
||||
|
||||
|
||||
def lidarr_monitored_mbids_key() -> str:
|
||||
return f"{LIDARR_PREFIX}monitored_mbids"
|
||||
|
||||
|
||||
def lidarr_status_key() -> str:
|
||||
return f"{LIDARR_PREFIX}status"
|
||||
|
||||
|
|
|
|||
4
backend/infrastructure/cache/disk_cache.py
vendored
4
backend/infrastructure/cache/disk_cache.py
vendored
|
|
@ -18,6 +18,8 @@ def _decode_json(text: str) -> Any:
|
|||
|
||||
|
||||
class DiskMetadataCache:
|
||||
_CACHE_VERSION = "v3"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_path: Path,
|
||||
|
|
@ -57,7 +59,7 @@ class DiskMetadataCache:
|
|||
|
||||
@staticmethod
|
||||
def _cache_hash(identifier: str) -> str:
|
||||
return hashlib.sha1(identifier.encode()).hexdigest()
|
||||
return hashlib.sha1(f"{DiskMetadataCache._CACHE_VERSION}:{identifier}".encode()).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def _meta_path(file_path: Path) -> Path:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class AlbumInfo(AppStruct):
|
|||
total_length: int | None = None
|
||||
in_library: bool = False
|
||||
requested: bool = False
|
||||
monitored: bool = False
|
||||
cover_url: str | None = None
|
||||
album_thumb_url: str | None = None
|
||||
album_back_url: str | None = None
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class ReleaseItem(AppStruct):
|
|||
year: int | None = None
|
||||
in_library: bool = False
|
||||
requested: bool = False
|
||||
monitored: bool = False
|
||||
|
||||
|
||||
class ArtistInfo(AppStruct):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class SearchResult(AppStruct):
|
|||
year: int | None = None
|
||||
in_library: bool = False
|
||||
requested: bool = False
|
||||
monitored: bool = False
|
||||
cover_url: str | None = None
|
||||
album_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}")
|
||||
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:
|
||||
t0 = time.monotonic()
|
||||
if not musicbrainz_id or not isinstance(musicbrainz_id, str):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import time
|
|||
from typing import Any, Optional
|
||||
from core.config import Settings
|
||||
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.http.deduplication import get_deduplicator
|
||||
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.clear_prefix(f"{LIDARR_PREFIX}library:")
|
||||
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:
|
||||
return await self._request("POST", endpoint, json_data=data)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from infrastructure.cache.cache_keys import (
|
|||
lidarr_artist_mbids_key,
|
||||
lidarr_library_grouped_key,
|
||||
lidarr_requested_mbids_key,
|
||||
lidarr_monitored_mbids_key,
|
||||
)
|
||||
from .base import LidarrBase
|
||||
|
||||
|
|
@ -31,11 +32,17 @@ class LidarrLibraryRepository(LidarrBase):
|
|||
|
||||
for item in data:
|
||||
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:
|
||||
filtered_count += 1
|
||||
continue
|
||||
|
||||
if not has_files:
|
||||
filtered_count += 1
|
||||
continue
|
||||
|
||||
artist_data = item.get("artist", {})
|
||||
artist = artist_data.get("artistName", "Unknown")
|
||||
artist_mbid = artist_data.get("foreignArtistId")
|
||||
|
|
@ -92,11 +99,17 @@ class LidarrLibraryRepository(LidarrBase):
|
|||
|
||||
for item in albums_data:
|
||||
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:
|
||||
filtered_count += 1
|
||||
continue
|
||||
|
||||
if not has_files:
|
||||
filtered_count += 1
|
||||
continue
|
||||
|
||||
artist_data = item.get("artist", {})
|
||||
artist_mbid = artist_data.get("foreignArtistId")
|
||||
artist_name = artist_data.get("artistName", "Unknown")
|
||||
|
|
@ -139,6 +152,10 @@ class LidarrLibraryRepository(LidarrBase):
|
|||
grouped: dict[str, list[LibraryGroupedAlbum]] = {}
|
||||
|
||||
for item in data:
|
||||
statistics = item.get("statistics", {})
|
||||
if statistics.get("trackFileCount", 0) == 0:
|
||||
continue
|
||||
|
||||
artist = item.get("artist", {}).get("artistName", "Unknown")
|
||||
title = item.get("title")
|
||||
year = None
|
||||
|
|
@ -182,9 +199,6 @@ class LidarrLibraryRepository(LidarrBase):
|
|||
data = await self._get_all_albums_raw()
|
||||
ids: set[str] = set()
|
||||
for item in data:
|
||||
if not item.get("monitored", False):
|
||||
continue
|
||||
|
||||
statistics = item.get("statistics", {})
|
||||
track_file_count = statistics.get("trackFileCount", 0)
|
||||
if track_file_count == 0:
|
||||
|
|
@ -212,7 +226,9 @@ class LidarrLibraryRepository(LidarrBase):
|
|||
data = await self._get("/api/v1/artist")
|
||||
ids: set[str] = set()
|
||||
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
|
||||
mbid = item.get("foreignArtistId") or item.get("mbId")
|
||||
if isinstance(mbid, str):
|
||||
|
|
@ -232,7 +248,7 @@ class LidarrLibraryRepository(LidarrBase):
|
|||
|
||||
async def get_monitored_no_files_mbids(self) -> set[str]:
|
||||
"""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)
|
||||
if cached_result is not None:
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ class LidarrRepositoryProtocol(Protocol):
|
|||
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:
|
||||
...
|
||||
|
||||
|
|
@ -92,3 +95,6 @@ class LidarrRepositoryProtocol(Protocol):
|
|||
self, artist_mbid: str, *, monitored: bool, monitor_new_items: str = "none",
|
||||
) -> 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.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.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.disk_cache import DiskMetadataCache
|
||||
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)
|
||||
|
||||
def _check_lidarr_in_library(self, lidarr_album: dict | None) -> bool:
|
||||
if lidarr_album and lidarr_album.get("monitored", False):
|
||||
statistics = lidarr_album.get("statistics", {})
|
||||
return statistics.get("trackFileCount", 0) > 0
|
||||
return False
|
||||
if lidarr_album is None:
|
||||
return False
|
||||
statistics = lidarr_album.get("statistics", {})
|
||||
return statistics.get("trackFileCount", 0) > 0
|
||||
|
||||
async def warm_full_album_cache(self, release_group_id: str) -> None:
|
||||
"""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)
|
||||
|
||||
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:
|
||||
release_group_id = validate_mbid(release_group_id, "album")
|
||||
except ValueError as e:
|
||||
|
|
@ -215,7 +256,7 @@ class AlbumService:
|
|||
future: asyncio.Future[AlbumInfo] = loop.create_future()
|
||||
self._album_in_flight[release_group_id] = future
|
||||
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():
|
||||
future.set_result(album_info)
|
||||
return album_info
|
||||
|
|
@ -232,14 +273,16 @@ class AlbumService:
|
|||
raise ResourceNotFoundError(f"Failed to get album info: {e}")
|
||||
|
||||
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:
|
||||
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)
|
||||
if in_library and lidarr_album:
|
||||
album_info = await self._build_album_from_lidarr(release_group_id, lidarr_album)
|
||||
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, release_group_id, album_info.artist_name, album_info.title,
|
||||
allow_fetch=True, is_monitored=album_info.in_library,
|
||||
|
|
@ -286,14 +329,21 @@ class AlbumService:
|
|||
disambiguation=cached_album_info.disambiguation,
|
||||
in_library=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,
|
||||
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
|
||||
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.monitored = is_monitored
|
||||
if not basic.album_thumb_url:
|
||||
basic.album_thumb_url = await self._get_audiodb_album_thumb(
|
||||
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)
|
||||
in_library = cached_album is not None
|
||||
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(
|
||||
release_group_id, basic.artist_name, basic.title,
|
||||
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
|
||||
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:
|
||||
album_id = lidarr_album.get("id")
|
||||
|
|
@ -428,9 +479,9 @@ class AlbumService:
|
|||
|
||||
return rg_result
|
||||
|
||||
async def _check_in_library(self, release_group_id: str, monitored_mbids: set[str] = None) -> bool:
|
||||
if monitored_mbids is not None:
|
||||
return release_group_id.lower() in monitored_mbids
|
||||
async def _check_in_library(self, release_group_id: str, library_mbids: set[str] = None) -> bool:
|
||||
if library_mbids is not None:
|
||||
return release_group_id.lower() in library_mbids
|
||||
|
||||
library_mbids = await self._lidarr_repo.get_library_mbids(include_release_ids=True)
|
||||
return release_group_id.lower() in library_mbids
|
||||
|
|
@ -557,13 +608,14 @@ class AlbumService:
|
|||
total_tracks=len(tracks),
|
||||
total_length=total_length if total_length > 0 else None,
|
||||
in_library=True,
|
||||
monitored=lidarr_album.get("monitored", False),
|
||||
cover_url=cover_url,
|
||||
)
|
||||
|
||||
async def _build_album_from_musicbrainz(
|
||||
self,
|
||||
release_group_id: str,
|
||||
monitored_mbids: set[str] = None
|
||||
library_mbids: set[str] = None
|
||||
) -> AlbumInfo:
|
||||
cached_album = await self._library_db.get_album_by_mbid(release_group_id)
|
||||
in_library = cached_album is not None
|
||||
|
|
@ -573,7 +625,7 @@ class AlbumService:
|
|||
artist_name, artist_id = extract_artist_info(release_group)
|
||||
|
||||
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(
|
||||
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"),
|
||||
"in_library": in_library,
|
||||
"requested": is_requested and not in_library,
|
||||
"monitored": lidarr_album.get("monitored", False),
|
||||
"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"),
|
||||
"in_library": in_library,
|
||||
"requested": is_requested and not in_library,
|
||||
"monitored": False,
|
||||
"cover_url": None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,8 +218,15 @@ class ArtistService:
|
|||
library_album_mbids: dict[str, Any] | None,
|
||||
) -> ArtistInfo:
|
||||
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)
|
||||
if in_library and lidarr_artist:
|
||||
use_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 [])
|
||||
)
|
||||
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)
|
||||
else:
|
||||
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_releases: bool = True,
|
||||
) -> 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
|
||||
)
|
||||
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)
|
||||
info = build_base_artist_info(
|
||||
mb_artist, artist_id, in_library,
|
||||
|
|
@ -440,9 +447,10 @@ class ArtistService:
|
|||
if not self._lidarr_repo.is_configured():
|
||||
return
|
||||
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_requested_mbids(),
|
||||
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||
self._lidarr_repo.get_artist_mbids(),
|
||||
)
|
||||
for release_list in (artist_info.albums, artist_info.singles, artist_info.eps):
|
||||
|
|
@ -452,6 +460,7 @@ class ArtistService:
|
|||
continue
|
||||
rg.in_library = rg_id in library_mbids
|
||||
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()
|
||||
is_in_artist_mbids = mbid_lower in artist_mbids
|
||||
artist_info.in_library = is_in_artist_mbids
|
||||
|
|
@ -530,12 +539,20 @@ class ArtistService:
|
|||
) -> ArtistReleases:
|
||||
try:
|
||||
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_requested_mbids(),
|
||||
self._get_library_cache_mbids(),
|
||||
self._lidarr_repo.get_monitored_no_files_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_secondary_types = set(t.lower() for t in prefs.secondary_types)
|
||||
|
||||
if in_library:
|
||||
if has_lidarr_data:
|
||||
if offset == 0:
|
||||
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)
|
||||
|
|
@ -567,7 +584,7 @@ class ArtistService:
|
|||
|
||||
return await self._filter_aware_release_page(
|
||||
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
|
||||
logger.error(f"Error fetching releases for artist {artist_id} at offset {offset}: {e}")
|
||||
|
|
@ -586,6 +603,7 @@ class ArtistService:
|
|||
requested_mbids: set[str],
|
||||
included_primary_types: set[str],
|
||||
included_secondary_types: set[str],
|
||||
monitored_mbids: set[str] | None = None,
|
||||
) -> ArtistReleases:
|
||||
if not included_primary_types:
|
||||
return ArtistReleases(
|
||||
|
|
@ -618,7 +636,7 @@ class ArtistService:
|
|||
temp_artist = {"release-group-list": release_groups}
|
||||
page_albums, page_singles, page_eps = categorize_release_groups(
|
||||
temp_artist, album_mbids, included_primary_types,
|
||||
included_secondary_types, requested_mbids,
|
||||
included_secondary_types, requested_mbids, monitored_mbids,
|
||||
)
|
||||
|
||||
for item in page_albums:
|
||||
|
|
@ -669,24 +687,27 @@ class ArtistService:
|
|||
artist_id: str,
|
||||
library_artist_mbids: set[str] = 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:
|
||||
mb_artist = await self._mb_repo.get_artist_by_id(artist_id)
|
||||
library_mbids = library_artist_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_monitored_no_files_mbids(),
|
||||
return_exceptions=True,
|
||||
)
|
||||
requested_mbids = requested_result[0] if not isinstance(requested_result[0], BaseException) else set()
|
||||
if isinstance(requested_result[0], BaseException):
|
||||
logger.warning(f"Lidarr unavailable, proceeding without requested data: {requested_result[0]}")
|
||||
requested_mbids = requested_result if not isinstance(requested_result, BaseException) else set()
|
||||
monitored_mbids = monitored_result if not isinstance(monitored_result, BaseException) else set()
|
||||
if isinstance(requested_result, BaseException):
|
||||
logger.warning(f"Lidarr unavailable, proceeding without requested data: {requested_result}")
|
||||
else:
|
||||
mb_artist, *lidarr_results = await asyncio.gather(
|
||||
self._mb_repo.get_artist_by_id(artist_id),
|
||||
self._lidarr_repo.get_artist_mbids(),
|
||||
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
||||
self._lidarr_repo.get_requested_mbids(),
|
||||
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||
return_exceptions=True,
|
||||
)
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
album_mbids = album_mbids | cache_mbids
|
||||
|
||||
if not mb_artist:
|
||||
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]:
|
||||
external_links_data = extract_external_links(mb_artist)
|
||||
|
|
@ -720,7 +740,8 @@ class ArtistService:
|
|||
self,
|
||||
mb_artist: dict[str, Any],
|
||||
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]]:
|
||||
prefs = self._preferences_service.get_preferences()
|
||||
included_primary_types = set(t.lower() for t in prefs.primary_types)
|
||||
|
|
@ -730,7 +751,8 @@ class ArtistService:
|
|||
album_mbids,
|
||||
included_primary_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]]:
|
||||
|
|
|
|||
|
|
@ -106,11 +106,14 @@ def categorize_release_groups(
|
|||
included_primary_types: Optional[set[str]] = None,
|
||||
included_secondary_types: 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]]]:
|
||||
if included_primary_types is None:
|
||||
included_primary_types = {"album", "single", "ep", "broadcast", "other"}
|
||||
if requested_mbids is None:
|
||||
requested_mbids = set()
|
||||
if monitored_mbids is None:
|
||||
monitored_mbids = set()
|
||||
albums: list[ReleaseItem] = []
|
||||
singles: list[ReleaseItem] = []
|
||||
eps: list[ReleaseItem] = []
|
||||
|
|
@ -130,6 +133,7 @@ def categorize_release_groups(
|
|||
rg_id_lower = rg_id.lower() if rg_id else ""
|
||||
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
|
||||
is_monitored = rg_id_lower in monitored_mbids if rg_id and not in_library and not requested else False
|
||||
rg_data = ReleaseItem(
|
||||
id=rg_id,
|
||||
title=rg.get("title"),
|
||||
|
|
@ -137,6 +141,7 @@ def categorize_release_groups(
|
|||
first_release_date=rg.get("first-release-date"),
|
||||
in_library=in_library,
|
||||
requested=requested,
|
||||
monitored=is_monitored,
|
||||
)
|
||||
if date := rg_data.first_release_date:
|
||||
try:
|
||||
|
|
@ -182,6 +187,7 @@ def categorize_lidarr_albums(
|
|||
track_file_count = album.get("track_file_count", 0)
|
||||
in_library = track_file_count > 0 or (mbid_lower in _cache_mbids)
|
||||
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(
|
||||
id=mbid,
|
||||
title=album.get("title"),
|
||||
|
|
@ -190,6 +196,7 @@ def categorize_lidarr_albums(
|
|||
year=album.get("year"),
|
||||
in_library=in_library,
|
||||
requested=requested,
|
||||
monitored=is_monitored,
|
||||
)
|
||||
if album_type == "album":
|
||||
albums.append(album_data)
|
||||
|
|
|
|||
|
|
@ -167,6 +167,13 @@ class DiscoverHomepageService:
|
|||
|
||||
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(
|
||||
lb_enabled, username, jf_enabled,
|
||||
resolved_source=resolved_source,
|
||||
|
|
@ -214,8 +221,8 @@ class DiscoverHomepageService:
|
|||
tasks["jf_most_played"] = self._jf_repo.get_most_played_artists(limit=50)
|
||||
|
||||
if lidarr_configured:
|
||||
tasks["library_artists"] = self._lidarr_repo.get_artists_from_library()
|
||||
tasks["library_albums"] = self._lidarr_repo.get_library()
|
||||
tasks["library_artists"] = self._lidarr_repo.get_artists_from_library(include_unmonitored=True)
|
||||
tasks["library_albums"] = self._lidarr_repo.get_library(include_unmonitored=True)
|
||||
|
||||
results = await self._execute_tasks(tasks)
|
||||
|
||||
|
|
@ -231,15 +238,15 @@ class DiscoverHomepageService:
|
|||
)
|
||||
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] = {
|
||||
"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(
|
||||
results, library_mbids
|
||||
results, library_mbids, monitored_mbids
|
||||
),
|
||||
"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:
|
||||
|
|
@ -464,7 +471,8 @@ class DiscoverHomepageService:
|
|||
return sections
|
||||
|
||||
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:
|
||||
releases = results.get("lb_fresh")
|
||||
if not releases:
|
||||
|
|
@ -475,16 +483,22 @@ class DiscoverHomepageService:
|
|||
if isinstance(r, dict):
|
||||
mbid = r.get("release_group_mbid", "")
|
||||
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(
|
||||
mbid=mbid,
|
||||
name=r.get("title", r.get("release_group_name", "Unknown")),
|
||||
artist_name=r.get("artist_credit_name", r.get("artist_name", "")),
|
||||
artist_mbid=artist_mbids[0] if artist_mbids else None,
|
||||
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:
|
||||
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
|
||||
logger.debug(f"Skipping fresh release item: {e}")
|
||||
continue
|
||||
|
|
@ -498,7 +512,8 @@ class DiscoverHomepageService:
|
|||
)
|
||||
|
||||
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:
|
||||
library_artists = results.get("library_artists") or []
|
||||
library_albums = results.get("library_albums") or []
|
||||
|
|
@ -551,6 +566,7 @@ class DiscoverHomepageService:
|
|||
artist_name=rg.artist_name,
|
||||
listen_count=rg.listen_count,
|
||||
in_library=False,
|
||||
monitored=bool(monitored_mbids) and rg_mbid.lower() in monitored_mbids,
|
||||
))
|
||||
artist_missing += 1
|
||||
|
||||
|
|
@ -904,6 +920,7 @@ class DiscoverHomepageService:
|
|||
self,
|
||||
results: dict[str, Any],
|
||||
library_mbids: set[str],
|
||||
monitored_mbids: set[str] | None = None,
|
||||
) -> HomeSection | None:
|
||||
albums = results.get("lfm_weekly_albums") or []
|
||||
if not albums:
|
||||
|
|
@ -914,7 +931,7 @@ class DiscoverHomepageService:
|
|||
|
||||
items = []
|
||||
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:
|
||||
home_album.mbid = rg_map.get(home_album.mbid, home_album.mbid)
|
||||
items.append(home_album)
|
||||
|
|
@ -933,6 +950,7 @@ class DiscoverHomepageService:
|
|||
self,
|
||||
results: dict[str, Any],
|
||||
library_mbids: set[str],
|
||||
monitored_mbids: set[str] | None = None,
|
||||
) -> HomeSection | None:
|
||||
tracks = results.get("lfm_recent") or []
|
||||
if not tracks:
|
||||
|
|
@ -944,7 +962,7 @@ class DiscoverHomepageService:
|
|||
items = []
|
||||
seen_album_mbids: set[str] = set()
|
||||
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:
|
||||
resolved = rg_map.get(home_album.mbid, home_album.mbid)
|
||||
home_album.mbid = resolved
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ class MbidResolutionService:
|
|||
if not lidarr_configured:
|
||||
return set()
|
||||
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")}
|
||||
except Exception: # noqa: BLE001
|
||||
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
|
||||
) -> GenreDetailResponse:
|
||||
lidarr_results = await asyncio.gather(
|
||||
self._lidarr_repo.get_artists_from_library(),
|
||||
self._lidarr_repo.get_library(),
|
||||
self._lidarr_repo.get_artists_from_library(include_unmonitored=True),
|
||||
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||
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:
|
||||
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_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_album_mbids = {a.musicbrainz_id.lower() for a in library_albums if a.musicbrainz_id}
|
||||
library_section = None
|
||||
|
|
@ -177,6 +179,7 @@ class HomeChartsService:
|
|||
image_url=None,
|
||||
release_date=str(result.year) if result.year else None,
|
||||
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
|
||||
]
|
||||
|
|
@ -203,7 +206,7 @@ class HomeChartsService:
|
|||
if resolved == "lastfm" and self._lfm_repo:
|
||||
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")}
|
||||
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}
|
||||
|
|
@ -249,7 +252,7 @@ class HomeChartsService:
|
|||
offset=offset,
|
||||
)
|
||||
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(
|
||||
range_=range_key, count=limit + 1, offset=offset
|
||||
),
|
||||
|
|
@ -275,15 +278,19 @@ class HomeChartsService:
|
|||
if resolved == "lastfm" and self._lfm_repo:
|
||||
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}
|
||||
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"]
|
||||
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)
|
||||
response_data = {}
|
||||
for r in ranges:
|
||||
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
|
||||
items = albums[1:limit] if len(albums) > 1 else []
|
||||
response_data[r] = PopularTimeRange(
|
||||
|
|
@ -317,14 +324,17 @@ class HomeChartsService:
|
|||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
library_albums, lb_albums = await asyncio.gather(
|
||||
self._lidarr_repo.get_library(),
|
||||
library_albums, lb_albums, monitored_mbids_result = await asyncio.gather(
|
||||
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||
self._lb_repo.get_sitewide_top_release_groups(
|
||||
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}
|
||||
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
|
||||
items = albums[:limit]
|
||||
return PopularAlbumsRangeResponse(
|
||||
|
|
@ -337,7 +347,7 @@ class HomeChartsService:
|
|||
)
|
||||
|
||||
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")}
|
||||
lfm_artists = await self._lfm_repo.get_global_top_artists(limit=limit + 1)
|
||||
artists = [
|
||||
|
|
@ -366,10 +376,15 @@ class HomeChartsService:
|
|||
|
||||
async def _get_popular_albums_lastfm(self, limit: int = 10) -> PopularAlbumsResponse:
|
||||
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 = {
|
||||
(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()
|
||||
if lfm_username:
|
||||
tasks = {
|
||||
|
|
@ -408,6 +423,7 @@ class HomeChartsService:
|
|||
image_url=album.image_url or None,
|
||||
listen_count=album.playcount,
|
||||
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",
|
||||
)
|
||||
for album in lfm_albums
|
||||
|
|
@ -433,7 +449,7 @@ class HomeChartsService:
|
|||
total_to_fetch = min(limit + offset + 1, 200)
|
||||
lfm_artists, library_artists = await asyncio.gather(
|
||||
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")}
|
||||
artists = [
|
||||
|
|
@ -470,17 +486,20 @@ class HomeChartsService:
|
|||
)
|
||||
|
||||
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(
|
||||
lfm_username,
|
||||
period=self._lastfm_period_for_range(range_key),
|
||||
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 = {
|
||||
(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 = [
|
||||
HomeAlbum(
|
||||
mbid=album.mbid,
|
||||
|
|
@ -490,6 +509,7 @@ class HomeChartsService:
|
|||
image_url=album.image_url or None,
|
||||
listen_count=album.playcount,
|
||||
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",
|
||||
)
|
||||
for album in lfm_albums
|
||||
|
|
@ -521,10 +541,15 @@ class HomeChartsService:
|
|||
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 = {
|
||||
(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"]
|
||||
tasks = {
|
||||
r: self._lb_repo.get_user_top_release_groups(
|
||||
|
|
@ -536,7 +561,7 @@ class HomeChartsService:
|
|||
response_data: dict[str, PopularTimeRange] = {}
|
||||
for r in ranges:
|
||||
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(
|
||||
range_key=r,
|
||||
label=HomeDataTransformers.get_range_label(r),
|
||||
|
|
@ -578,16 +603,19 @@ class HomeChartsService:
|
|||
has_more=False,
|
||||
)
|
||||
|
||||
library_albums, rgs = await asyncio.gather(
|
||||
self._lidarr_repo.get_library(),
|
||||
library_albums, rgs, monitored_mbids_result = await asyncio.gather(
|
||||
self._lidarr_repo.get_library(include_unmonitored=True),
|
||||
self._lb_repo.get_user_top_release_groups(
|
||||
username=lb_username, 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
|
||||
}
|
||||
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
|
||||
items = albums[:limit]
|
||||
return PopularAlbumsRangeResponse(
|
||||
|
|
|
|||
|
|
@ -162,9 +162,10 @@ class HomeService:
|
|||
)
|
||||
|
||||
if lidarr_configured:
|
||||
tasks["library_albums"] = self._lidarr_repo.get_library()
|
||||
tasks["library_artists"] = self._lidarr_repo.get_artists_from_library()
|
||||
tasks["library_albums"] = self._lidarr_repo.get_library(include_unmonitored=True)
|
||||
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["monitored_mbids"] = self._lidarr_repo.get_monitored_no_files_mbids()
|
||||
|
||||
if resolved_source == "listenbrainz" and lb_enabled and username:
|
||||
lb_settings = self._preferences.get_listenbrainz_connection()
|
||||
|
|
@ -195,6 +196,7 @@ class HomeService:
|
|||
library_album_mbids = {
|
||||
(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)
|
||||
|
||||
|
|
@ -207,10 +209,10 @@ class HomeService:
|
|||
results, library_artist_mbids
|
||||
)
|
||||
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(
|
||||
results, library_album_mbids
|
||||
results, library_album_mbids, monitored_mbids
|
||||
)
|
||||
response.recently_played = self._builders.build_listenbrainz_recent_section(results)
|
||||
response.favorite_artists = self._builders.build_listenbrainz_favorites_section(results)
|
||||
|
|
@ -220,7 +222,7 @@ class HomeService:
|
|||
results, library_artist_mbids
|
||||
)
|
||||
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.favorite_artists = self._builders.build_lastfm_favorites_section(results)
|
||||
|
|
|
|||
|
|
@ -60,24 +60,24 @@ class HomeSectionBuilders:
|
|||
)
|
||||
|
||||
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:
|
||||
albums = results.get("lb_trending_albums") or []
|
||||
return HomeSection(
|
||||
title="Popular Right Now",
|
||||
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,
|
||||
)
|
||||
|
||||
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:
|
||||
release_groups = results.get("lb_user_top_rgs") or []
|
||||
if not release_groups:
|
||||
return None
|
||||
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]
|
||||
]
|
||||
return HomeSection(
|
||||
|
|
@ -95,7 +95,8 @@ class HomeSectionBuilders:
|
|||
return HomeSection(title="Browse by Genre", type="genres", items=genres, source=source)
|
||||
|
||||
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:
|
||||
releases = results.get("lb_fresh")
|
||||
if not releases:
|
||||
|
|
@ -103,7 +104,7 @@ class HomeSectionBuilders:
|
|||
return HomeSection(
|
||||
title="New From Artists You Follow",
|
||||
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",
|
||||
)
|
||||
|
||||
|
|
@ -170,12 +171,12 @@ class HomeSectionBuilders:
|
|||
)
|
||||
|
||||
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:
|
||||
albums = results.get("lfm_top_albums") or []
|
||||
items = [
|
||||
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]
|
||||
)
|
||||
if a is not None
|
||||
|
|
|
|||
|
|
@ -70,9 +70,15 @@ class HomeDataTransformers:
|
|||
def lb_release_to_home(
|
||||
self,
|
||||
release: ListenBrainzReleaseGroup,
|
||||
library_mbids: set[str]
|
||||
library_mbids: set[str],
|
||||
monitored_mbids: set[str] | None = None,
|
||||
) -> HomeAlbum:
|
||||
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(
|
||||
mbid=release.release_group_mbid,
|
||||
name=release.release_group_name,
|
||||
|
|
@ -81,7 +87,8 @@ class HomeDataTransformers:
|
|||
image_url=None,
|
||||
release_date=None,
|
||||
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(
|
||||
|
|
@ -130,7 +137,13 @@ class HomeDataTransformers:
|
|||
self,
|
||||
album: LastFmAlbum,
|
||||
library_mbids: set[str],
|
||||
monitored_mbids: set[str] | None = 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(
|
||||
mbid=None,
|
||||
name=album.name,
|
||||
|
|
@ -138,7 +151,8 @@ class HomeDataTransformers:
|
|||
artist_mbid=None,
|
||||
image_url=album.image_url or None,
|
||||
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",
|
||||
)
|
||||
|
||||
|
|
@ -159,14 +173,21 @@ class HomeDataTransformers:
|
|||
self,
|
||||
track: LastFmRecentTrack,
|
||||
library_mbids: set[str],
|
||||
monitored_mbids: set[str] | None = 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(
|
||||
mbid=track.album_mbid,
|
||||
name=track.album_name or track.track_name,
|
||||
artist_name=track.artist_name,
|
||||
artist_mbid=track.artist_mbid,
|
||||
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",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -167,6 +167,15 @@ class LibraryService:
|
|||
except Exception as e: # noqa: BLE001
|
||||
logger.error(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]:
|
||||
try:
|
||||
|
|
@ -348,8 +357,8 @@ class LibraryService:
|
|||
|
||||
sync_succeeded = False
|
||||
try:
|
||||
albums = await self._lidarr_repo.get_library()
|
||||
artists = await self._lidarr_repo.get_artists_from_library()
|
||||
albums = await self._lidarr_repo.get_library(include_unmonitored=True)
|
||||
artists = await self._lidarr_repo.get_artists_from_library(include_unmonitored=True)
|
||||
|
||||
albums_data = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class AlbumPhase:
|
|||
async def precache_album_data(
|
||||
self,
|
||||
release_group_ids: list[str],
|
||||
monitored_mbids: set[str],
|
||||
library_mbids: set[str],
|
||||
status_service: CacheStatusService,
|
||||
library_album_mbids: dict[str, Any] = None,
|
||||
offset: int = 0,
|
||||
|
|
@ -48,11 +48,11 @@ class AlbumPhase:
|
|||
cached_info = await album_service._cache.get(cache_key)
|
||||
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 album_service.get_album_info(rgid, monitored_mbids=monitored_mbids)
|
||||
await album_service.get_album_info(rgid, library_mbids=library_mbids)
|
||||
metadata_fetched = True
|
||||
else:
|
||||
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")
|
||||
file_path = self._cover_repo.cache_dir / f"{cache_filename}.bin"
|
||||
if not file_path.exists():
|
||||
|
|
|
|||
|
|
@ -210,12 +210,12 @@ class LibraryPrecacheService:
|
|||
if status_service.is_cancelled():
|
||||
return
|
||||
|
||||
monitored_mbids: set[str] = set()
|
||||
library_album_mbids_set: set[str] = set()
|
||||
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
|
||||
if not is_unknown_mbid(mbid):
|
||||
monitored_mbids.add(mbid.lower())
|
||||
deduped_release_groups = list(monitored_mbids)
|
||||
library_album_mbids_set.add(mbid.lower())
|
||||
deduped_release_groups = list(library_album_mbids_set)
|
||||
if status_service.is_cancelled():
|
||||
return
|
||||
items_needing_metadata = []
|
||||
|
|
@ -234,7 +234,7 @@ class LibraryPrecacheService:
|
|||
for rgid in deduped_release_groups:
|
||||
if rgid in processed_albums:
|
||||
continue
|
||||
if rgid.lower() in monitored_mbids:
|
||||
if rgid.lower() in library_album_mbids_set:
|
||||
cache_filename = get_cache_filename(f"rg_{rgid}", "500")
|
||||
file_path = self._cover_repo.cache_dir / f"{cache_filename}.bin"
|
||||
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)
|
||||
if items_to_process:
|
||||
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:
|
||||
await status_service.skip_phase('albums', generation=generation)
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ class SearchService:
|
|||
limits["albums"] = limit_albums
|
||||
|
||||
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(
|
||||
query,
|
||||
limits=limits,
|
||||
|
|
@ -173,10 +173,11 @@ class SearchService:
|
|||
),
|
||||
self._lidarr_repo.get_library_mbids(include_release_ids=True),
|
||||
self._lidarr_repo.get_queue(),
|
||||
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
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:
|
||||
logger.warning("MusicBrainz search returned no results or failed")
|
||||
|
|
@ -188,10 +189,13 @@ class SearchService:
|
|||
else:
|
||||
queued_mbids = set()
|
||||
|
||||
monitored_mbids = monitored_mbids_raw or set()
|
||||
|
||||
for item in grouped.get("albums", []):
|
||||
mbid_lower = (item.musicbrainz_id or "").lower()
|
||||
item.in_library = mbid_lower in library_mbids
|
||||
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", [])
|
||||
await self._apply_audiodb_search_overlay(all_results)
|
||||
|
|
@ -246,9 +250,10 @@ class SearchService:
|
|||
return [], None
|
||||
|
||||
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_queue(),
|
||||
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||
)
|
||||
library_mbids = library_mbids_raw or set()
|
||||
if queue_items_raw:
|
||||
|
|
@ -256,10 +261,13 @@ class SearchService:
|
|||
else:
|
||||
queued_mbids = set()
|
||||
|
||||
monitored_mbids = monitored_mbids_raw or set()
|
||||
|
||||
for item in results:
|
||||
mbid_lower = (item.musicbrainz_id or "").lower()
|
||||
item.in_library = mbid_lower in library_mbids
|
||||
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)
|
||||
|
||||
|
|
@ -288,9 +296,10 @@ class SearchService:
|
|||
|
||||
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_queue(),
|
||||
self._lidarr_repo.get_monitored_no_files_mbids(),
|
||||
)
|
||||
library_mbids = library_mbids_raw or set()
|
||||
if queue_items_raw:
|
||||
|
|
@ -298,10 +307,13 @@ class SearchService:
|
|||
else:
|
||||
queued_mbids = set()
|
||||
|
||||
monitored_mbids = monitored_mbids_raw or set()
|
||||
|
||||
for item in grouped.get("albums", []):
|
||||
mbid_lower = (item.musicbrainz_id or "").lower()
|
||||
item.in_library = mbid_lower in library_mbids
|
||||
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] = []
|
||||
for item in grouped.get("artists", []) + grouped.get("albums", []):
|
||||
|
|
@ -313,6 +325,7 @@ class SearchService:
|
|||
musicbrainz_id=item.musicbrainz_id,
|
||||
in_library=item.in_library,
|
||||
requested=item.requested,
|
||||
monitored=item.monitored,
|
||||
disambiguation=item.disambiguation,
|
||||
score=item.score,
|
||||
))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ from infrastructure.cache.disk_cache import DiskMetadataCache
|
|||
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
|
||||
async def test_set_album_serializes_msgspec_struct_as_mapping(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)
|
||||
|
||||
cache_hash = hashlib.sha1(mbid.encode()).hexdigest()
|
||||
cache_hash = _cache_hash(mbid)
|
||||
cache_file = tmp_path / "persistent" / "albums" / f"{cache_hash}.json"
|
||||
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)
|
||||
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.parent.mkdir(parents=True, exist_ok=True)
|
||||
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["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"
|
||||
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["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"
|
||||
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)
|
||||
|
||||
cache_hash = hashlib.sha1(mbid.encode()).hexdigest()
|
||||
cache_hash = _cache_hash(mbid)
|
||||
persistent_file = tmp_path / "persistent" / "audiodb_artists" / f"{cache_hash}.json"
|
||||
recent_file = tmp_path / "recent" / "audiodb_artists" / f"{cache_hash}.json"
|
||||
assert persistent_file.exists()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ def _sample_album_data() -> list[dict]:
|
|||
"releaseDate": "2023-01-15",
|
||||
"added": "2023-01-10T12:00:00Z",
|
||||
"images": [],
|
||||
"statistics": {"trackFileCount": 5},
|
||||
"artist": {
|
||||
"artistName": "Artist A",
|
||||
"foreignArtistId": "artist-a-mbid",
|
||||
|
|
@ -39,6 +40,7 @@ def _sample_album_data() -> list[dict]:
|
|||
"releaseDate": "2024-06-01",
|
||||
"added": "2024-06-01T08:00:00Z",
|
||||
"images": [],
|
||||
"statistics": {"trackFileCount": 8},
|
||||
"artist": {
|
||||
"artistName": "Artist B",
|
||||
"foreignArtistId": "artist-b-mbid",
|
||||
|
|
@ -52,6 +54,7 @@ def _sample_album_data() -> list[dict]:
|
|||
"releaseDate": "2020-03-01",
|
||||
"added": "2020-03-01T00:00:00Z",
|
||||
"images": [],
|
||||
"statistics": {"trackFileCount": 3},
|
||||
"artist": {
|
||||
"artistName": "Artist C",
|
||||
"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")
|
||||
|
||||
assert result.in_library is False
|
||||
assert result.in_library is True
|
||||
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._get_cached_album_info = AsyncMock(return_value=None)
|
||||
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(
|
||||
return_value=[
|
||||
{
|
||||
|
|
@ -135,7 +135,7 @@ async def test_get_album_tracks_info_multi_disc_same_track_numbers():
|
|||
service, lidarr_repo, _ = _make_service()
|
||||
service._get_cached_album_info = AsyncMock(return_value=None)
|
||||
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(
|
||||
return_value=[
|
||||
{"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.get_library_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())
|
||||
|
||||
wikidata_repo = AsyncMock()
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ def _make_service(
|
|||
lidarr_repo.get_artist_details = AsyncMock(return_value=lidarr_artist)
|
||||
lidarr_repo.get_library_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())
|
||||
|
||||
wikidata_repo = AsyncMock()
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ def _search_service(audiodb: MagicMock | None = None) -> SearchService:
|
|||
lidarr_repo = MagicMock()
|
||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
||||
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||
coverart_repo = MagicMock()
|
||||
prefs = MagicMock()
|
||||
prefs.get_preferences.return_value = MagicMock(secondary_types=[])
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ def _search_service(audiodb=None) -> SearchService:
|
|||
lidarr_repo = MagicMock()
|
||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
||||
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||
coverart_repo = MagicMock()
|
||||
prefs = MagicMock()
|
||||
prefs.get_preferences.return_value = MagicMock(secondary_types=[])
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ def _make_service(
|
|||
else:
|
||||
lidarr_repo.get_queue = AsyncMock(return_value=queue_items or [])
|
||||
|
||||
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||
|
||||
coverart_repo = MagicMock()
|
||||
preferences_service = MagicMock()
|
||||
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.get_library_mbids = AsyncMock(return_value=set())
|
||||
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
||||
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||
|
||||
coverart_repo = MagicMock()
|
||||
preferences_service = MagicMock()
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ def _make_search_service(audiodb_service=None) -> SearchService:
|
|||
lidarr_repo = MagicMock()
|
||||
lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
|
||||
lidarr_repo.get_queue = AsyncMock(return_value=[])
|
||||
lidarr_repo.get_monitored_no_files_mbids = AsyncMock(return_value=set())
|
||||
coverart_repo = MagicMock()
|
||||
prefs = MagicMock()
|
||||
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):
|
||||
call_count = 0
|
||||
|
||||
async def fail_then_succeed():
|
||||
async def fail_then_succeed(**kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
|
|
@ -94,7 +94,7 @@ class TestSyncFutureDedup:
|
|||
call_count = 0
|
||||
sync_event = asyncio.Event()
|
||||
|
||||
async def slow_get_library():
|
||||
async def slow_get_library(**kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
sync_event.set()
|
||||
|
|
@ -118,7 +118,7 @@ class TestSyncFutureDedup:
|
|||
@pytest.mark.asyncio
|
||||
async def test_concurrent_sync_failure_propagates_to_waiter(self):
|
||||
"""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)
|
||||
raise RuntimeError("Lidarr DNS failure")
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
import { libraryStore } from '$lib/stores/library';
|
||||
import { integrationStore } from '$lib/stores/integration';
|
||||
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 { getListenTitle } from '$lib/utils/enrichment';
|
||||
import { Download, Music2 } from 'lucide-svelte';
|
||||
|
|
@ -29,6 +31,7 @@
|
|||
let listenTitle = $derived(getListenTitle(enrichmentSource, 'album'));
|
||||
|
||||
let requesting = $state(false);
|
||||
let monitoredLoading = $state(false);
|
||||
|
||||
let inLibrary = $derived(
|
||||
libraryStore.isInLibrary(album.musicbrainz_id) || album.in_library || false
|
||||
|
|
@ -38,6 +41,11 @@
|
|||
!album.in_library &&
|
||||
(album.requested || libraryStore.isRequested(album.musicbrainz_id))
|
||||
);
|
||||
let isMonitored = $derived(
|
||||
!inLibrary &&
|
||||
!isRequested &&
|
||||
(album.monitored || libraryStore.isMonitored(album.musicbrainz_id))
|
||||
);
|
||||
|
||||
async function handleRequest(e: Event) {
|
||||
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() {
|
||||
album.in_library = false;
|
||||
album.requested = false;
|
||||
album.monitored = false;
|
||||
album = album;
|
||||
onremoved?.();
|
||||
}
|
||||
|
|
@ -161,6 +183,15 @@
|
|||
artistName={album.artist || 'Unknown'}
|
||||
ondeleted={handleDeleted}
|
||||
/>
|
||||
{:else if isMonitored}
|
||||
<LibraryBadge
|
||||
status="monitored"
|
||||
musicbrainzId={album.musicbrainz_id}
|
||||
albumTitle={album.title}
|
||||
artistName={album.artist || 'Unknown'}
|
||||
ontogglemonitored={handleToggleMonitored}
|
||||
{monitoredLoading}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
musicbrainz_id: da.musicbrainz_id,
|
||||
in_library: da.in_library,
|
||||
requested: da.requested,
|
||||
monitored: da.monitored,
|
||||
cover_url: da.cover_url
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Check } from 'lucide-svelte';
|
||||
import { Check, Bookmark } from 'lucide-svelte';
|
||||
import AlbumImage from '$lib/components/AlbumImage.svelte';
|
||||
import AlbumCardOverlay from '$lib/components/AlbumCardOverlay.svelte';
|
||||
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">
|
||||
<Check class="w-3 h-3" />
|
||||
</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 album.mbid && album.in_library}
|
||||
<AlbumCardOverlay
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
ArrowRight,
|
||||
X,
|
||||
Check,
|
||||
Bookmark,
|
||||
Music2,
|
||||
Tv,
|
||||
Sparkles,
|
||||
|
|
@ -184,7 +185,7 @@
|
|||
{/if}
|
||||
{#if item.in_library}
|
||||
<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>
|
||||
{/if}
|
||||
</figure>
|
||||
|
|
@ -217,7 +218,11 @@
|
|||
/>
|
||||
{#if item.in_library}
|
||||
<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>
|
||||
{/if}
|
||||
{#if item.mbid && item.in_library}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
musicbrainz_id: rg.id,
|
||||
in_library: libraryStore.isInLibrary(rg.id) || rg.in_library,
|
||||
requested: rg.requested,
|
||||
monitored: rg.monitored,
|
||||
cover_url: null,
|
||||
type_info: rg.type
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
<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 { STATUS_COLORS } from '$lib/constants';
|
||||
import DeleteAlbumModal from './DeleteAlbumModal.svelte';
|
||||
import ArtistRemovedModal from './ArtistRemovedModal.svelte';
|
||||
|
||||
interface Props {
|
||||
status: 'library' | 'requested';
|
||||
status: 'library' | 'requested' | 'monitored';
|
||||
musicbrainzId: string;
|
||||
albumTitle: string;
|
||||
artistName: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
positioning?: string;
|
||||
ondeleted?: (result: { artist_removed: boolean; artist_name?: string | null }) => void;
|
||||
ontogglemonitored?: () => void;
|
||||
monitoredLoading?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -22,7 +24,9 @@
|
|||
artistName,
|
||||
size = 'md',
|
||||
positioning = '',
|
||||
ondeleted
|
||||
ondeleted,
|
||||
ontogglemonitored,
|
||||
monitoredLoading = false
|
||||
}: Props = $props();
|
||||
|
||||
let showDeleteModal = $state(false);
|
||||
|
|
@ -31,11 +35,11 @@
|
|||
|
||||
const sizeClasses = $derived(
|
||||
{
|
||||
sm: { button: 'p-0.5', icon: 'w-2.5 h-2.5', strokeWidth: status === 'library' ? '3' : '2' },
|
||||
md: { button: 'p-1.5', icon: 'h-4 w-4', 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-5 w-5', strokeWidth: status === 'library' ? '3' : '2' },
|
||||
lg: {
|
||||
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'
|
||||
}
|
||||
}[size]
|
||||
|
|
@ -44,11 +48,21 @@
|
|||
const lgButtonClass = $derived(
|
||||
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) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (status === 'monitored') {
|
||||
ontogglemonitored?.();
|
||||
return;
|
||||
}
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
|
|
@ -66,31 +80,62 @@
|
|||
class="{positioning} rounded-full shadow-sm transition-colors duration-200 group/badge {sizeClasses.button} {lgButtonClass}"
|
||||
style="background-color: {bgColor};"
|
||||
onclick={handleClick}
|
||||
onmouseenter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#ef4444';
|
||||
}}
|
||||
onmouseenter={status === 'monitored'
|
||||
? (e) => {
|
||||
e.currentTarget.style.filter = 'brightness(1.3)';
|
||||
}
|
||||
: (e) => {
|
||||
e.currentTarget.style.backgroundColor = '#ef4444';
|
||||
}}
|
||||
onmouseleave={(e) => {
|
||||
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
|
||||
class="{sizeClasses.icon} group-hover/badge:hidden"
|
||||
color={colors.secondary}
|
||||
strokeWidth={Number(sizeClasses.strokeWidth)}
|
||||
/>
|
||||
{:else}
|
||||
{:else if status === 'requested'}
|
||||
<Clock
|
||||
class="{sizeClasses.icon} group-hover/badge:hidden"
|
||||
color={colors.secondary}
|
||||
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}
|
||||
<Trash2 class="{sizeClasses.icon} hidden group-hover/badge:block" color="white" strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
{#if showDeleteModal}
|
||||
{#if status !== 'monitored' && showDeleteModal}
|
||||
<DeleteAlbumModal
|
||||
{albumTitle}
|
||||
{artistName}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import { ChevronDown, Download } from 'lucide-svelte';
|
||||
import { colors } from '$lib/colors';
|
||||
import { libraryStore } from '$lib/stores/library';
|
||||
import { toggleAlbumMonitored } from '$lib/utils/monitorAlbum';
|
||||
import { toastStore } from '$lib/stores/toast';
|
||||
import AlbumImage from './AlbumImage.svelte';
|
||||
import LibraryBadge from './LibraryBadge.svelte';
|
||||
|
||||
|
|
@ -12,6 +14,7 @@
|
|||
year?: number | null;
|
||||
in_library?: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
}
|
||||
|
||||
interface RemoveResult {
|
||||
|
|
@ -46,11 +49,31 @@
|
|||
function handleDeleted(rg: Release, result: RemoveResult) {
|
||||
rg.in_library = false;
|
||||
rg.requested = false;
|
||||
rg.monitored = false;
|
||||
releases = releases;
|
||||
onRemoved?.(result);
|
||||
}
|
||||
</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="bg-base-300 rounded-t-box">
|
||||
<button
|
||||
|
|
@ -105,30 +128,31 @@
|
|||
size="lg"
|
||||
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}
|
||||
<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="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>
|
||||
{@render requestButton(rg, `Request ${title.toLowerCase().slice(0, -1)}`)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -271,6 +271,9 @@
|
|||
{#if result.requested}
|
||||
<span class="badge badge-sm badge-warning">Requested</span>
|
||||
{/if}
|
||||
{#if result.monitored && !result.in_library && !result.requested}
|
||||
<span class="badge badge-sm badge-neutral">Monitored</span>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Check, Search } from 'lucide-svelte';
|
||||
import { Check, Bookmark, Search } from 'lucide-svelte';
|
||||
import type { HomeAlbum, HomeArtist } from '$lib/types';
|
||||
import { formatListenCount } from '$lib/utils/formatting';
|
||||
import AlbumImage from './AlbumImage.svelte';
|
||||
|
|
@ -82,6 +82,11 @@
|
|||
<Check class="h-3 w-3" />
|
||||
In Library
|
||||
</div>
|
||||
{:else if item.monitored && !('requested' in item && item.requested)}
|
||||
<div class="badge badge-neutral">
|
||||
<Bookmark class="h-3 w-3" />
|
||||
Monitored
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#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">
|
||||
<Check class="h-3 w-3" />
|
||||
</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 item.mbid && item.in_library}
|
||||
<AlbumCardOverlay
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
import { colors } from '$lib/colors';
|
||||
import { libraryStore } from '$lib/stores/library';
|
||||
import { requestAlbum } from '$lib/utils/albumRequest';
|
||||
import { toggleAlbumMonitored } from '$lib/utils/monitorAlbum';
|
||||
import { toastStore } from '$lib/stores/toast';
|
||||
import AlbumImage from './AlbumImage.svelte';
|
||||
import LibraryBadge from './LibraryBadge.svelte';
|
||||
import LastFmPlaceholder from './LastFmPlaceholder.svelte';
|
||||
|
|
@ -24,12 +26,14 @@
|
|||
|
||||
let libraryMbids = new SvelteSet<string>();
|
||||
let requestedMbids = new SvelteSet<string>();
|
||||
let monitoredMbids = new SvelteSet<string>();
|
||||
let storeInitialized = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = libraryStore.subscribe((state) => {
|
||||
libraryMbids = new SvelteSet(state.mbidSet);
|
||||
requestedMbids = new SvelteSet(state.requestedSet);
|
||||
monitoredMbids = new SvelteSet(state.monitoredSet);
|
||||
storeInitialized = state.initialized;
|
||||
});
|
||||
return unsubscribe;
|
||||
|
|
@ -48,6 +52,14 @@
|
|||
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 {
|
||||
return album.release_group_mbid ? requestingIds.has(album.release_group_mbid) : false;
|
||||
}
|
||||
|
|
@ -129,6 +141,26 @@
|
|||
size="sm"
|
||||
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}
|
||||
</div>
|
||||
|
||||
|
|
@ -143,7 +175,7 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{#if !isInLibrary(album) && !isRequested(album)}
|
||||
{#if !isInLibrary(album) && !isRequested(album) && !isMonitored(album)}
|
||||
<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"
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@ export const PLACEHOLDER_COLORS = {
|
|||
} as const;
|
||||
|
||||
export const STATUS_COLORS = {
|
||||
REQUESTED: '#F59E0B'
|
||||
REQUESTED: '#F59E0B',
|
||||
MONITORED: '#6B7280'
|
||||
} 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;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { api } from '$lib/api/client';
|
|||
export interface LibraryState {
|
||||
mbidSet: Set<string>;
|
||||
requestedSet: Set<string>;
|
||||
monitoredSet: Set<string>;
|
||||
loading: boolean;
|
||||
lastUpdated: number | null;
|
||||
initialized: boolean;
|
||||
|
|
@ -14,6 +15,7 @@ export interface LibraryState {
|
|||
const initialState: LibraryState = {
|
||||
mbidSet: new Set(),
|
||||
requestedSet: new Set(),
|
||||
monitoredSet: new Set(),
|
||||
loading: false,
|
||||
lastUpdated: null,
|
||||
initialized: false
|
||||
|
|
@ -22,6 +24,7 @@ const initialState: LibraryState = {
|
|||
type LibraryCacheData = {
|
||||
mbids: string[];
|
||||
requested: string[];
|
||||
monitored: string[];
|
||||
};
|
||||
|
||||
function createLibraryStore() {
|
||||
|
|
@ -33,18 +36,24 @@ function createLibraryStore() {
|
|||
|
||||
function normalizeCachedData(data: LibraryCacheData | string[]): LibraryCacheData {
|
||||
if (Array.isArray(data)) {
|
||||
return { mbids: data, requested: [] };
|
||||
return { mbids: data, requested: [], monitored: [] };
|
||||
}
|
||||
return {
|
||||
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({
|
||||
mbids: [...mbidSet],
|
||||
requested: [...requestedSet]
|
||||
requested: [...requestedSet],
|
||||
monitored: [...monitoredSet]
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -57,8 +66,9 @@ function createLibraryStore() {
|
|||
const normalized = normalizeCachedData(cached.data);
|
||||
const mbids = normalized.mbids.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);
|
||||
return;
|
||||
}
|
||||
|
|
@ -67,6 +77,7 @@ function createLibraryStore() {
|
|||
...s,
|
||||
mbidSet: new Set(mbids),
|
||||
requestedSet: new Set(requested),
|
||||
monitoredSet: new Set(monitored),
|
||||
lastUpdated: cached.timestamp,
|
||||
initialized: true
|
||||
}));
|
||||
|
|
@ -86,22 +97,26 @@ function createLibraryStore() {
|
|||
}
|
||||
|
||||
try {
|
||||
const data = await api.global.get<{ mbids?: string[]; requested_mbids?: string[] }>(
|
||||
'/api/v1/library/mbids'
|
||||
);
|
||||
const data = await api.global.get<{
|
||||
mbids?: string[];
|
||||
requested_mbids?: string[];
|
||||
monitored_mbids?: string[];
|
||||
}>('/api/v1/library/mbids');
|
||||
const mbids: string[] = (data.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) => ({
|
||||
...s,
|
||||
mbidSet: new Set(mbids),
|
||||
requestedSet: new Set(requested),
|
||||
monitoredSet: new Set(monitored),
|
||||
loading: false,
|
||||
lastUpdated: Date.now(),
|
||||
initialized: true
|
||||
}));
|
||||
|
||||
cache.set({ mbids, requested });
|
||||
cache.set({ mbids, requested, monitored });
|
||||
} catch {
|
||||
if (!background) {
|
||||
update((s) => ({ ...s, loading: false, initialized: true }));
|
||||
|
|
@ -121,8 +136,10 @@ function createLibraryStore() {
|
|||
newSet.add(mbid.toLowerCase());
|
||||
const newRequested = new Set(s.requestedSet);
|
||||
newRequested.delete(mbid.toLowerCase());
|
||||
persistState(newSet, newRequested);
|
||||
return { ...s, mbidSet: newSet, requestedSet: newRequested };
|
||||
const newMonitored = new Set(s.monitoredSet);
|
||||
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());
|
||||
const newRequested = new Set(s.requestedSet);
|
||||
newRequested.delete(mbid.toLowerCase());
|
||||
persistState(newSet, newRequested);
|
||||
return { ...s, mbidSet: newSet, requestedSet: newRequested };
|
||||
const newMonitored = new Set(s.monitoredSet);
|
||||
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())) {
|
||||
return s;
|
||||
}
|
||||
const newSet = new Set(s.requestedSet);
|
||||
newSet.add(mbid.toLowerCase());
|
||||
persistState(s.mbidSet, newSet);
|
||||
return { ...s, requestedSet: newSet };
|
||||
const lower = mbid.toLowerCase();
|
||||
const newRequested = new Set(s.requestedSet);
|
||||
newRequested.add(lower);
|
||||
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 {
|
||||
if (!mbid) return false;
|
||||
const lower = mbid.toLowerCase();
|
||||
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() {
|
||||
|
|
@ -179,6 +225,8 @@ function createLibraryStore() {
|
|||
removeMbid,
|
||||
isRequested,
|
||||
addRequested,
|
||||
isMonitored,
|
||||
setMonitored,
|
||||
updateCacheTTL: cache.updateTTL
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export type Album = {
|
|||
musicbrainz_id: string;
|
||||
in_library: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
cover_url?: string | null;
|
||||
album_thumb_url?: string | null;
|
||||
album_back_url?: string | null;
|
||||
|
|
@ -63,6 +64,7 @@ export type SuggestResult = {
|
|||
musicbrainz_id: string;
|
||||
in_library: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
disambiguation?: string | null;
|
||||
score: number;
|
||||
};
|
||||
|
|
@ -111,6 +113,7 @@ export type ReleaseGroup = {
|
|||
first_release_date?: string;
|
||||
in_library: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
};
|
||||
|
||||
export type ExternalLink = {
|
||||
|
|
@ -207,6 +210,7 @@ export type AlbumInfo = {
|
|||
total_length?: number | null;
|
||||
in_library: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
cover_url?: string | null;
|
||||
album_thumb_url?: string | null;
|
||||
album_back_url?: string | null;
|
||||
|
|
@ -229,6 +233,7 @@ export type AlbumBasicInfo = {
|
|||
disambiguation?: string | null;
|
||||
in_library: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
cover_url?: string | null;
|
||||
album_thumb_url?: string | null;
|
||||
};
|
||||
|
|
@ -274,6 +279,7 @@ export type HomeArtist = {
|
|||
image_url: string | null;
|
||||
listen_count: number | null;
|
||||
in_library: boolean;
|
||||
monitored?: boolean;
|
||||
};
|
||||
|
||||
export type HomeAlbum = {
|
||||
|
|
@ -286,6 +292,7 @@ export type HomeAlbum = {
|
|||
listen_count: number | null;
|
||||
in_library: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
};
|
||||
|
||||
export type HomeTrack = {
|
||||
|
|
@ -506,6 +513,7 @@ export type SimilarArtist = {
|
|||
name: string;
|
||||
listen_count: number;
|
||||
in_library: boolean;
|
||||
monitored?: boolean;
|
||||
image_url?: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -552,6 +560,7 @@ export type TopAlbum = {
|
|||
listen_count: number;
|
||||
in_library: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
cover_url?: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -569,6 +578,7 @@ export type DiscoveryAlbum = {
|
|||
year?: number | null;
|
||||
in_library: boolean;
|
||||
requested?: boolean;
|
||||
monitored?: boolean;
|
||||
cover_url?: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -587,6 +597,7 @@ export type DiscoverQueueItemLight = {
|
|||
recommendation_reason: string;
|
||||
is_wildcard: boolean;
|
||||
in_library: boolean;
|
||||
monitored?: boolean;
|
||||
};
|
||||
|
||||
export type DiscoverQueueEnrichment = {
|
||||
|
|
@ -747,6 +758,7 @@ export type RequestHistoryItem = {
|
|||
completed_at?: string | null;
|
||||
status: string;
|
||||
in_library: boolean;
|
||||
monitored?: boolean;
|
||||
};
|
||||
|
||||
export type ActiveRequestsResponse = {
|
||||
|
|
|
|||
7
frontend/src/lib/utils/monitorAlbum.ts
Normal file
7
frontend/src/lib/utils/monitorAlbum.ts
Normal file
|
|
@ -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}
|
||||
inLibrary={state.inLibrary}
|
||||
isRequested={state.isRequested}
|
||||
albumMonitored={state.albumMonitored}
|
||||
requesting={state.requesting}
|
||||
refreshing={state.refreshing}
|
||||
pollingForSources={state.pollingForSources}
|
||||
lidarrConfigured={$integrationStore.lidarr}
|
||||
monitorToggleLoading={state.monitorToggleLoading}
|
||||
artistMonitored={state.artistMonitored}
|
||||
artistInLidarr={state.artistInLidarr}
|
||||
onrequest={state.handleRequest}
|
||||
ondelete={state.handleDeleteClick}
|
||||
onrefresh={state.refreshAll}
|
||||
onartistclick={state.goToArtist}
|
||||
ontogglemonitored={state.handleToggleMonitored}
|
||||
/>
|
||||
|
||||
{#if state.loadingTracks}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import AlbumImage from '$lib/components/AlbumImage.svelte';
|
||||
import HeroBackdrop from '$lib/components/HeroBackdrop.svelte';
|
||||
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 {
|
||||
album: AlbumBasicInfo;
|
||||
|
|
@ -13,16 +13,20 @@
|
|||
loadingTracks: boolean;
|
||||
inLibrary: boolean;
|
||||
isRequested: boolean;
|
||||
albumMonitored: boolean;
|
||||
requesting: boolean;
|
||||
refreshing: boolean;
|
||||
pollingForSources: boolean;
|
||||
lidarrConfigured: boolean;
|
||||
monitorToggleLoading?: boolean;
|
||||
|
||||
artistMonitored?: boolean;
|
||||
artistInLidarr?: boolean;
|
||||
onrequest: (opts?: { monitorArtist?: boolean; autoDownloadArtist?: boolean }) => void;
|
||||
ondelete: () => void;
|
||||
onrefresh: () => void;
|
||||
onartistclick: () => void;
|
||||
ontogglemonitored: (monitored: boolean) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -31,15 +35,19 @@
|
|||
loadingTracks,
|
||||
inLibrary,
|
||||
isRequested,
|
||||
albumMonitored,
|
||||
requesting,
|
||||
refreshing,
|
||||
pollingForSources,
|
||||
lidarrConfigured,
|
||||
monitorToggleLoading = false,
|
||||
artistMonitored = false,
|
||||
artistInLidarr = false,
|
||||
onrequest,
|
||||
ondelete,
|
||||
onrefresh,
|
||||
onartistclick
|
||||
onartistclick,
|
||||
ontogglemonitored
|
||||
}: Props = $props();
|
||||
|
||||
let monitorArtist = $state(false);
|
||||
|
|
@ -59,6 +67,8 @@
|
|||
? getApiUrl(`/api/v1/covers/release-group/${album.musicbrainz_id}?size=250`)
|
||||
: null)
|
||||
);
|
||||
|
||||
const showMonitorToggle = $derived(lidarrConfigured && artistInLidarr);
|
||||
</script>
|
||||
|
||||
<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">
|
||||
{#if (inLibrary || isRequested) && lidarrConfigured}
|
||||
{#if (inLibrary || isRequested || albumMonitored) && lidarrConfigured}
|
||||
<button
|
||||
class="absolute top-3 right-3 btn btn-sm btn-ghost btn-circle z-20"
|
||||
onclick={onrefresh}
|
||||
|
|
@ -223,6 +233,23 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/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>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { artistHref } from '$lib/utils/entityRoutes';
|
|||
import type { AlbumBasicInfo, YouTubeTrackLink, YouTubeLink, YouTubeQuotaStatus } from '$lib/types';
|
||||
import { compareDiscTrack, getDiscTrackKey } from '$lib/player/queueHelpers';
|
||||
import { requestAlbum } from '$lib/utils/albumRequest';
|
||||
import { toggleAlbumMonitored } from '$lib/utils/monitorAlbum';
|
||||
|
||||
export interface EventHandlerDeps {
|
||||
getAlbum: () => AlbumBasicInfo | null;
|
||||
|
|
@ -18,6 +19,7 @@ export interface EventHandlerDeps {
|
|||
setShowDeleteModal: (v: boolean) => void;
|
||||
setShowArtistRemovedModal: (v: boolean) => void;
|
||||
setRemovedArtistName: (v: string) => void;
|
||||
setMonitorToggleLoading: (v: boolean) => void;
|
||||
setToast: (msg: string, type: 'success' | 'error' | 'info' | 'warning') => void;
|
||||
setShowToast: (v: boolean) => void;
|
||||
onRequestSuccess?: (opts?: { monitorArtist?: boolean; autoDownloadArtist?: boolean }) => void;
|
||||
|
|
@ -85,6 +87,7 @@ export function createEventHandlers(deps: EventHandlerDeps) {
|
|||
if (album) {
|
||||
album.in_library = false;
|
||||
album.requested = false;
|
||||
album.monitored = false;
|
||||
deps.setAlbum(album);
|
||||
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 {
|
||||
const album = deps.getAlbum();
|
||||
// eslint-disable-next-line svelte/no-navigation-without-resolve -- artistHref uses resolve() internally
|
||||
|
|
@ -110,6 +133,7 @@ export function createEventHandlers(deps: EventHandlerDeps) {
|
|||
handleRequest,
|
||||
handleDeleteClick,
|
||||
handleDeleted,
|
||||
handleToggleMonitored,
|
||||
goToArtist
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||
let playlistModalRef = $state<{ open: (tracks: QueueItem[]) => void } | null>(null);
|
||||
let abortController: AbortController | null = null;
|
||||
let refreshing = $state(false);
|
||||
let monitorToggleLoading = $state(false);
|
||||
let pollingForSources = $state(false);
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let artistInLidarr = $state(false);
|
||||
|
|
@ -130,6 +131,17 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||
const isRequested = $derived(
|
||||
!!(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() {
|
||||
if (abortController) {
|
||||
|
|
@ -562,6 +574,7 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||
setShowDeleteModal: (v) => (showDeleteModal = v),
|
||||
setShowArtistRemovedModal: (v) => (showArtistRemovedModal = v),
|
||||
setRemovedArtistName: (v) => (removedArtistName = v),
|
||||
setMonitorToggleLoading: (v) => (monitorToggleLoading = v),
|
||||
setToast: (msg, type) => {
|
||||
toastMessage = msg;
|
||||
toastType = type;
|
||||
|
|
@ -800,6 +813,12 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||
get isRequested() {
|
||||
return isRequested;
|
||||
},
|
||||
get isMonitored() {
|
||||
return isMonitored;
|
||||
},
|
||||
get albumMonitored() {
|
||||
return albumMonitored;
|
||||
},
|
||||
get artistInLidarr() {
|
||||
return artistInLidarr;
|
||||
},
|
||||
|
|
@ -809,6 +828,9 @@ export function createAlbumPageState(albumIdGetter: () => string) {
|
|||
get refreshing() {
|
||||
return refreshing;
|
||||
},
|
||||
get monitorToggleLoading() {
|
||||
return monitorToggleLoading;
|
||||
},
|
||||
get pollingForSources() {
|
||||
return pollingForSources;
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue