Trim wishlist runtime plumbing

- remove the redundant wishlist-service injection from the runtime wrappers
- keep the package owning its own singleton service access
- simplify the route runtime API and update the wishlist tests to match
This commit is contained in:
Antti Kettunen 2026-04-28 21:52:21 +03:00
parent 7f3272f3ba
commit 0125f478fc
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
6 changed files with 28 additions and 46 deletions

View file

@ -11,6 +11,7 @@ from typing import Any, Callable, Dict
from core.wishlist.payloads import build_failed_track_wishlist_context from core.wishlist.payloads import build_failed_track_wishlist_context
from core.wishlist.selection import filter_wishlist_tracks_by_category, sanitize_and_dedupe_wishlist_tracks from core.wishlist.selection import filter_wishlist_tracks_by_category, sanitize_and_dedupe_wishlist_tracks
from core.wishlist.service import get_wishlist_service
from core.wishlist.state import get_wishlist_cycle, set_wishlist_cycle from core.wishlist.state import get_wishlist_cycle, set_wishlist_cycle
from utils.logging_config import get_logger from utils.logging_config import get_logger
@ -26,7 +27,6 @@ class WishlistAutoProcessingRuntime:
processing_guard: Callable[[], AbstractContextManager[bool]] processing_guard: Callable[[], AbstractContextManager[bool]]
is_actually_processing: Callable[[], bool] is_actually_processing: Callable[[], bool]
app_context_factory: Callable[[], AbstractContextManager[Any]] app_context_factory: Callable[[], AbstractContextManager[Any]]
get_wishlist_service: Callable[[], Any]
get_profiles_database: Callable[[], Any] get_profiles_database: Callable[[], Any]
get_music_database: Callable[[], Any] get_music_database: Callable[[], Any]
download_batches: Dict[str, Dict[str, Any]] download_batches: Dict[str, Dict[str, Any]]
@ -292,7 +292,6 @@ def remove_tracks_already_in_library(
class WishlistManualDownloadRuntime: class WishlistManualDownloadRuntime:
"""Dependencies needed to start a manual wishlist download batch outside the controller.""" """Dependencies needed to start a manual wishlist download batch outside the controller."""
get_wishlist_service: Callable[[], Any]
get_music_database: Callable[[], Any] get_music_database: Callable[[], Any]
download_batches: Dict[str, Dict[str, Any]] download_batches: Dict[str, Dict[str, Any]]
tasks_lock: Any tasks_lock: Any
@ -316,7 +315,7 @@ def start_manual_wishlist_download_batch(
logger = runtime.logger logger = runtime.logger
try: try:
wishlist_service = runtime.get_wishlist_service() wishlist_service = get_wishlist_service()
db = runtime.get_music_database() db = runtime.get_music_database()
manual_profile_id = runtime.profile_id manual_profile_id = runtime.profile_id
@ -471,7 +470,7 @@ def process_wishlist_automatically(runtime: WishlistAutoProcessingRuntime, autom
return return
with runtime.app_context_factory(): with runtime.app_context_factory():
wishlist_service = runtime.get_wishlist_service() wishlist_service = get_wishlist_service()
# Check if wishlist has tracks across all profiles # Check if wishlist has tracks across all profiles
database = runtime.get_profiles_database() database = runtime.get_profiles_database()
@ -627,7 +626,6 @@ def automatic_wishlist_cleanup_after_db_update(
"""Remove wishlist entries that already exist in the library after a DB update.""" """Remove wishlist entries that already exist in the library after a DB update."""
try: try:
from config.settings import config_manager from config.settings import config_manager
from core.wishlist.service import get_wishlist_service
from database.music_database import MusicDatabase, get_database from database.music_database import MusicDatabase, get_database
wishlist_service = wishlist_service or get_wishlist_service() wishlist_service = wishlist_service or get_wishlist_service()

View file

@ -10,6 +10,7 @@ from typing import Any, Callable, Dict
from core.wishlist.reporting import build_wishlist_stats_payload from core.wishlist.reporting import build_wishlist_stats_payload
from core.wishlist.selection import prepare_wishlist_tracks_for_display from core.wishlist.selection import prepare_wishlist_tracks_for_display
from core.wishlist.service import get_wishlist_service
from core.wishlist.state import get_wishlist_cycle as _get_wishlist_cycle from core.wishlist.state import get_wishlist_cycle as _get_wishlist_cycle
from core.wishlist.state import set_wishlist_cycle as _set_wishlist_cycle from core.wishlist.state import set_wishlist_cycle as _set_wishlist_cycle
from utils.logging_config import get_logger from utils.logging_config import get_logger
@ -23,13 +24,11 @@ logger = module_logger
class WishlistRouteRuntime: class WishlistRouteRuntime:
"""Dependencies needed to service wishlist HTTP endpoints outside the controller.""" """Dependencies needed to service wishlist HTTP endpoints outside the controller."""
get_wishlist_service: Callable[[], Any]
get_music_database: Callable[[], Any] get_music_database: Callable[[], Any]
get_current_profile_id: Callable[[], int] profile_id: int
download_batches: Dict[str, Dict[str, Any]] download_batches: Dict[str, Dict[str, Any]]
download_tasks: Dict[str, Dict[str, Any]] download_tasks: Dict[str, Dict[str, Any]]
tasks_lock: Any tasks_lock: Any
is_wishlist_auto_processing_flag: Callable[[], bool]
is_wishlist_actually_processing: Callable[[], bool] is_wishlist_actually_processing: Callable[[], bool]
reset_wishlist_processing_state: Callable[[], None] reset_wishlist_processing_state: Callable[[], None]
add_activity_item: Callable[[Any, Any, Any, Any], Any] add_activity_item: Callable[[Any, Any, Any, Any], Any]
@ -113,7 +112,7 @@ def process_wishlist_api(
) -> tuple[Dict[str, Any], int]: ) -> tuple[Dict[str, Any], int]:
"""Trigger wishlist processing in the background.""" """Trigger wishlist processing in the background."""
try: try:
if runtime.is_wishlist_auto_processing_flag(): if runtime.is_wishlist_actually_processing():
return {"success": False, "error": "Wishlist processing already in progress"}, 409 return {"success": False, "error": "Wishlist processing already in progress"}, 409
thread = runtime.thread_factory(target=start_processing, daemon=True) thread = runtime.thread_factory(target=start_processing, daemon=True)
@ -127,8 +126,7 @@ def process_wishlist_api(
def get_wishlist_count(runtime: WishlistRouteRuntime) -> tuple[Dict[str, Any], int]: def get_wishlist_count(runtime: WishlistRouteRuntime) -> tuple[Dict[str, Any], int]:
"""Return the current wishlist count for the active profile.""" """Return the current wishlist count for the active profile."""
try: try:
wishlist_service = runtime.get_wishlist_service() count = get_wishlist_service().get_wishlist_count(profile_id=runtime.profile_id)
count = wishlist_service.get_wishlist_count(profile_id=runtime.get_current_profile_id())
return {"count": count}, 200 return {"count": count}, 200
except Exception as exc: except Exception as exc:
runtime.logger.error("Error getting wishlist count: %s", exc) runtime.logger.error("Error getting wishlist count: %s", exc)
@ -138,8 +136,7 @@ def get_wishlist_count(runtime: WishlistRouteRuntime) -> tuple[Dict[str, Any], i
def get_wishlist_stats(runtime: WishlistRouteRuntime) -> tuple[Dict[str, Any], int]: def get_wishlist_stats(runtime: WishlistRouteRuntime) -> tuple[Dict[str, Any], int]:
"""Return wishlist statistics for the UI.""" """Return wishlist statistics for the UI."""
try: try:
wishlist_service = runtime.get_wishlist_service() raw_tracks = get_wishlist_service().get_wishlist_tracks_for_download(profile_id=runtime.profile_id)
raw_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=runtime.get_current_profile_id())
next_run_in_seconds = runtime.get_next_run_seconds("process_wishlist") if runtime.get_next_run_seconds else 0 next_run_in_seconds = runtime.get_next_run_seconds("process_wishlist") if runtime.get_next_run_seconds else 0
is_processing = runtime.is_wishlist_actually_processing() is_processing = runtime.is_wishlist_actually_processing()
current_cycle = _get_wishlist_cycle(runtime.get_music_database) current_cycle = _get_wishlist_cycle(runtime.get_music_database)
@ -188,7 +185,6 @@ def get_wishlist_tracks(
) -> tuple[Dict[str, Any], int]: ) -> tuple[Dict[str, Any], int]:
"""Return wishlist tracks for the modal UI.""" """Return wishlist tracks for the modal UI."""
try: try:
wishlist_service = runtime.get_wishlist_service()
db = runtime.get_music_database() db = runtime.get_music_database()
with runtime.tasks_lock: with runtime.tasks_lock:
@ -198,13 +194,13 @@ def get_wishlist_tracks(
) )
if not wishlist_batch_active: if not wishlist_batch_active:
duplicates_removed = db.remove_wishlist_duplicates(profile_id=runtime.get_current_profile_id()) duplicates_removed = db.remove_wishlist_duplicates(profile_id=runtime.profile_id)
if duplicates_removed > 0: if duplicates_removed > 0:
runtime.logger.warning("Cleaned %s duplicate tracks from wishlist", duplicates_removed) runtime.logger.warning("Cleaned %s duplicate tracks from wishlist", duplicates_removed)
else: else:
runtime.logger.warning("Skipping wishlist duplicate cleanup - download in progress") runtime.logger.warning("Skipping wishlist duplicate cleanup - download in progress")
raw_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=runtime.get_current_profile_id()) raw_tracks = get_wishlist_service().get_wishlist_tracks_for_download(profile_id=runtime.profile_id)
prepared = prepare_wishlist_tracks_for_display(raw_tracks, category=category, limit=limit) prepared = prepare_wishlist_tracks_for_display(raw_tracks, category=category, limit=limit)
if prepared["duplicates_found"] > 0: if prepared["duplicates_found"] > 0:
@ -232,8 +228,7 @@ def get_wishlist_tracks(
def clear_wishlist(runtime: WishlistRouteRuntime) -> tuple[Dict[str, Any], int]: def clear_wishlist(runtime: WishlistRouteRuntime) -> tuple[Dict[str, Any], int]:
"""Clear the wishlist and cancel active wishlist batches.""" """Clear the wishlist and cancel active wishlist batches."""
try: try:
wishlist_service = runtime.get_wishlist_service() success = get_wishlist_service().clear_wishlist(profile_id=runtime.profile_id)
success = wishlist_service.clear_wishlist(profile_id=runtime.get_current_profile_id())
if success: if success:
cancelled_count = 0 cancelled_count = 0
@ -282,10 +277,9 @@ def remove_track_from_wishlist(
if not spotify_track_id: if not spotify_track_id:
return {"success": False, "error": "No spotify_track_id provided"}, 400 return {"success": False, "error": "No spotify_track_id provided"}, 400
wishlist_service = runtime.get_wishlist_service() success = get_wishlist_service().remove_track_from_wishlist(
success = wishlist_service.remove_track_from_wishlist(
spotify_track_id, spotify_track_id,
profile_id=runtime.get_current_profile_id(), profile_id=runtime.profile_id,
) )
if success: if success:
@ -310,8 +304,8 @@ def remove_album_from_wishlist(
if not album_id and not album_name_filter: if not album_id and not album_name_filter:
return {"success": False, "error": "No album_id or album_name provided"}, 400 return {"success": False, "error": "No album_id or album_name provided"}, 400
wishlist_service = runtime.get_wishlist_service() wishlist_service = get_wishlist_service()
all_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=runtime.get_current_profile_id()) all_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=runtime.profile_id)
tracks_to_remove = [] tracks_to_remove = []
for track in all_tracks: for track in all_tracks:
@ -334,7 +328,7 @@ def remove_album_from_wishlist(
tracks_to_remove.append(spotify_track_id) tracks_to_remove.append(spotify_track_id)
removed_count = 0 removed_count = 0
album_remove_pid = runtime.get_current_profile_id() album_remove_pid = runtime.profile_id
for spotify_track_id in tracks_to_remove: for spotify_track_id in tracks_to_remove:
if wishlist_service.remove_track_from_wishlist(spotify_track_id, profile_id=album_remove_pid): if wishlist_service.remove_track_from_wishlist(spotify_track_id, profile_id=album_remove_pid):
removed_count += 1 removed_count += 1
@ -363,11 +357,10 @@ def remove_batch_from_wishlist(
if not spotify_track_ids or not isinstance(spotify_track_ids, list): if not spotify_track_ids or not isinstance(spotify_track_ids, list):
return {"success": False, "error": "Missing or invalid spotify_track_ids"}, 400 return {"success": False, "error": "Missing or invalid spotify_track_ids"}, 400
wishlist_service = runtime.get_wishlist_service()
removed = 0 removed = 0
pid = runtime.get_current_profile_id() pid = runtime.profile_id
for track_id in spotify_track_ids: for track_id in spotify_track_ids:
if wishlist_service.remove_track_from_wishlist(track_id, profile_id=pid): if get_wishlist_service().remove_track_from_wishlist(track_id, profile_id=pid):
removed += 1 removed += 1
runtime.logger.info("Batch removed %s track(s) from wishlist", removed) runtime.logger.info("Batch removed %s track(s) from wishlist", removed)
@ -406,13 +399,12 @@ def add_album_track_to_wishlist(
"added_via": "library_wishlist_modal", "added_via": "library_wishlist_modal",
} }
wishlist_service = runtime.get_wishlist_service() success = get_wishlist_service().add_spotify_track_to_wishlist(
success = wishlist_service.add_spotify_track_to_wishlist(
spotify_track_data=spotify_track_data, spotify_track_data=spotify_track_data,
failure_reason="Added from library (incomplete album)", failure_reason="Added from library (incomplete album)",
source_type=source_type, source_type=source_type,
source_context=enhanced_source_context, source_context=enhanced_source_context,
profile_id=runtime.get_current_profile_id(), profile_id=runtime.profile_id,
) )
if success: if success:

View file

@ -1,6 +1,7 @@
from contextlib import contextmanager from contextlib import contextmanager
from types import SimpleNamespace from types import SimpleNamespace
from core.wishlist import processing
from core.wishlist.processing import WishlistAutoProcessingRuntime, process_wishlist_automatically from core.wishlist.processing import WishlistAutoProcessingRuntime, process_wishlist_automatically
@ -139,6 +140,7 @@ def _build_runtime(
batch_map = {} batch_map = {}
wishlist_service = _FakeWishlistService(tracks, count=count) wishlist_service = _FakeWishlistService(tracks, count=count)
processing.get_wishlist_service = lambda: wishlist_service
profiles_db = _FakeProfilesDatabase(profiles or [{"id": 1}]) profiles_db = _FakeProfilesDatabase(profiles or [{"id": 1}])
music_db = _FakeMusicDatabase(cycle_value=cycle_value) music_db = _FakeMusicDatabase(cycle_value=cycle_value)
executor = _FakeExecutor() executor = _FakeExecutor()
@ -162,7 +164,6 @@ def _build_runtime(
runtime = WishlistAutoProcessingRuntime( runtime = WishlistAutoProcessingRuntime(
processing_guard=guard, processing_guard=guard,
app_context_factory=app_context, app_context_factory=app_context,
get_wishlist_service=lambda: wishlist_service,
get_profiles_database=lambda: profiles_db, get_profiles_database=lambda: profiles_db,
get_music_database=lambda: music_db, get_music_database=lambda: music_db,
download_batches=batch_map, download_batches=batch_map,

View file

@ -80,6 +80,7 @@ class _FakeLock:
def _build_runtime(tracks, owned_matches=None, batch_map=None): def _build_runtime(tracks, owned_matches=None, batch_map=None):
wishlist_service = _FakeWishlistService(tracks) wishlist_service = _FakeWishlistService(tracks)
processing.get_wishlist_service = lambda: wishlist_service
music_db = _FakeMusicDatabase(owned_matches=owned_matches) music_db = _FakeMusicDatabase(owned_matches=owned_matches)
executor = _FakeExecutor() executor = _FakeExecutor()
logger = _FakeLogger() logger = _FakeLogger()
@ -87,7 +88,6 @@ def _build_runtime(tracks, owned_matches=None, batch_map=None):
batch_map = batch_map or {} batch_map = batch_map or {}
runtime = WishlistManualDownloadRuntime( runtime = WishlistManualDownloadRuntime(
get_wishlist_service=lambda: wishlist_service,
get_music_database=lambda: music_db, get_music_database=lambda: music_db,
download_batches=batch_map, download_batches=batch_map,
tasks_lock=_FakeLock(), tasks_lock=_FakeLock(),

View file

@ -1,5 +1,6 @@
import json import json
import core.wishlist.routes as routes_module
from core.wishlist.routes import ( from core.wishlist.routes import (
WishlistRouteRuntime, WishlistRouteRuntime,
add_album_track_to_wishlist, add_album_track_to_wishlist,
@ -148,7 +149,6 @@ def _build_runtime(
cycle_value="albums", cycle_value="albums",
duplicate_removals=0, duplicate_removals=0,
clear_result=True, clear_result=True,
auto_processing_flag=False,
actually_processing=False, actually_processing=False,
next_run_seconds=0, next_run_seconds=0,
download_batches=None, download_batches=None,
@ -157,17 +157,16 @@ def _build_runtime(
reset_callback=None, reset_callback=None,
): ):
service = _FakeWishlistService(tracks=tracks, count=count, clear_result=clear_result) service = _FakeWishlistService(tracks=tracks, count=count, clear_result=clear_result)
routes_module.get_wishlist_service = lambda: service
db = _FakeMusicDatabase(cycle_value=cycle_value, duplicate_removals=duplicate_removals) db = _FakeMusicDatabase(cycle_value=cycle_value, duplicate_removals=duplicate_removals)
logger = _FakeLogger() logger = _FakeLogger()
activity_calls = [] activity_calls = []
runtime = WishlistRouteRuntime( runtime = WishlistRouteRuntime(
get_wishlist_service=lambda: service,
get_music_database=lambda: db, get_music_database=lambda: db,
get_current_profile_id=lambda: 1, profile_id=1,
download_batches=download_batches if download_batches is not None else {}, download_batches=download_batches if download_batches is not None else {},
download_tasks=download_tasks if download_tasks is not None else {}, download_tasks=download_tasks if download_tasks is not None else {},
tasks_lock=_FakeLock(), tasks_lock=_FakeLock(),
is_wishlist_auto_processing_flag=lambda: auto_processing_flag,
is_wishlist_actually_processing=lambda: actually_processing, is_wishlist_actually_processing=lambda: actually_processing,
reset_wishlist_processing_state=reset_callback or (lambda: None), reset_wishlist_processing_state=reset_callback or (lambda: None),
add_activity_item=lambda *args: activity_calls.append(args), add_activity_item=lambda *args: activity_calls.append(args),
@ -200,7 +199,7 @@ def test_process_wishlist_api_starts_background_thread_when_idle():
def test_process_wishlist_api_rejects_when_flag_is_set(): def test_process_wishlist_api_rejects_when_flag_is_set():
thread_factory = _FakeThreadFactory() thread_factory = _FakeThreadFactory()
runtime, _service, _db, logger, _activity_calls = _build_runtime( runtime, _service, _db, logger, _activity_calls = _build_runtime(
auto_processing_flag=True, actually_processing=True,
thread_factory=thread_factory, thread_factory=thread_factory,
) )

View file

@ -17529,7 +17529,6 @@ def _process_wishlist_automatically(automation_id=None):
processing_guard=_processing_guard, processing_guard=_processing_guard,
is_actually_processing=is_wishlist_actually_processing, is_actually_processing=is_wishlist_actually_processing,
app_context_factory=lambda: app.app_context(), app_context_factory=lambda: app.app_context(),
get_wishlist_service=get_wishlist_service,
get_profiles_database=get_database, get_profiles_database=get_database,
get_music_database=MusicDatabase, get_music_database=MusicDatabase,
download_batches=download_batches, download_batches=download_batches,
@ -18344,22 +18343,18 @@ def get_wishlist_tracks():
def _build_wishlist_route_runtime( def _build_wishlist_route_runtime(
*, *,
is_auto_processing_flag=None,
is_actually_processing_fn=None, is_actually_processing_fn=None,
reset_wishlist_processing_state=None, reset_wishlist_processing_state=None,
get_next_run_seconds=None, get_next_run_seconds=None,
): ):
from core.wishlist_service import get_wishlist_service
from database.music_database import MusicDatabase from database.music_database import MusicDatabase
return _WishlistRouteRuntime( return _WishlistRouteRuntime(
get_wishlist_service=get_wishlist_service,
get_music_database=MusicDatabase, get_music_database=MusicDatabase,
get_current_profile_id=get_current_profile_id, profile_id=get_current_profile_id(),
download_batches=download_batches, download_batches=download_batches,
download_tasks=download_tasks, download_tasks=download_tasks,
tasks_lock=tasks_lock, tasks_lock=tasks_lock,
is_wishlist_auto_processing_flag=is_auto_processing_flag or (lambda: wishlist_auto_processing),
is_wishlist_actually_processing=is_actually_processing_fn or is_wishlist_actually_processing, is_wishlist_actually_processing=is_actually_processing_fn or is_wishlist_actually_processing,
reset_wishlist_processing_state=reset_wishlist_processing_state or (lambda: None), reset_wishlist_processing_state=reset_wishlist_processing_state or (lambda: None),
add_activity_item=add_activity_item, add_activity_item=add_activity_item,
@ -18385,14 +18380,11 @@ def start_wishlist_missing_downloads():
}), 409 }), 409
data = request.get_json() or {} data = request.get_json() or {}
from core.wishlist_service import get_wishlist_service
from database.music_database import MusicDatabase from database.music_database import MusicDatabase
wishlist_service = get_wishlist_service()
db = MusicDatabase() db = MusicDatabase()
manual_profile_id = get_current_profile_id() manual_profile_id = get_current_profile_id()
manual_runtime = _WishlistManualDownloadRuntime( manual_runtime = _WishlistManualDownloadRuntime(
get_wishlist_service=lambda: wishlist_service,
get_music_database=lambda: db, get_music_database=lambda: db,
download_batches=download_batches, download_batches=download_batches,
tasks_lock=tasks_lock, tasks_lock=tasks_lock,