23c9125ad8
Backend CI / Lint (push) Waiting to run
Backend CI / Tests (push) Waiting to run
Squashes 26 incremental fork commits (Apr–May 2026) onto upstream main as a single
diff for cleaner cross-fork comparison. Original history preserved on the
pre-squash-backup tag locally.
Feature additions
─────────────────
• Inline single-track download via yt-dlp-worker proxy
New routes: POST /api/v1/track-download/search (source: youtube | spotify),
POST /api/v1/track-download, GET /api/v1/track-download/{id}. Frontend
TrackDownloadButton in album track list AND popular-songs row, with a per-button
source picker. Per-user rate limits live in the worker's SQLite store. On
completion the backend fires Lidarr RefreshArtist + Plex library refresh +
cache invalidation, and the popular-songs list auto-refreshes.
• Per-instance library pinning via MUSICSEERR_LIBRARY env
Backend stamps the library label server-side (music / music-personal /
music-shared); clients cannot override. Drives an instance-segregated
deployment of three musicseerr containers sharing one source tree.
• Lidarr-request flow (single-track requests via Lidarr indexers)
New routes: POST /api/v1/lidarr-request, GET /api/v1/lidarr-request/status.
Per-album asyncio.Lock keyed on album_mbid so rapid-clicks on the same album
serialize correctly. Cross-release track matcher with foreignTrackId →
foreignRecordingId → position+disc → exact-title → substring fallback chain,
evaluated per release (recording UUIDs frequently differ between album,
single, and deluxe edition releases of the same song). Flips
artist.monitored = True on request so Lidarr's WantedAlbums query reaches
the track. Full Lidarr-chain gate (artist AND album AND track) for the
status endpoint to avoid false-positive REQUESTED display. Persistent UI
state so button icons survive refresh and cross-album navigation.
• Privacy: show_now_playing toggle in Settings → Home
Default off. Plex /status/sessions returns active audio sessions across the
whole server with no library-section filter, so a shared instance leaks
every household member's listening activity. The merged store still emits
the user's local MusicSeerr playback bar; only server-derived sessions
(Plex / Jellyfin / Navidrome) are gated.
• Per-button visibility prefs for the track-row action cluster
Settings → Preferences → Download Options / Playback Buttons. Per-context
(popular_songs / album_page) force-off flags layered on top of the existing
source-availability gate.
• UX: wrap action cluster on mobile, hide LidarrRequestButton in tight
layouts, cross-album status-leak fix in AlbumTrackList ($effect keyed on
album.musicbrainz_id to rebuild lookup; map keyed by
"{albumMbid}:{position}:{disc}").
Test coverage
─────────────
Backend pytest: full suite green (2031/2031 as of squash). New: schema-default
tests for HomeSettings, lidarr_request_service cross-release matcher
regression test, singleton-registry expected-count bump to 59. Frontend
vitest: SettingsHome.svelte.spec covers new toggle, nowPlayingSessions
.svelte.spec covers the privacy gate (no fetch when off; fetches when on).
87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
"""Tests for the dependencies package structure and registry."""
|
|
|
|
import importlib
|
|
|
|
import pytest
|
|
|
|
from core.dependencies._registry import _singleton_registry, clear_all_singletons
|
|
|
|
|
|
class TestSingletonRegistry:
|
|
def test_registry_has_expected_count(self):
|
|
# Bumped from 57 to 59 for the fork-added singletons:
|
|
# get_track_download_service (yt-dlp proxy)
|
|
# get_lidarr_request_service (single-track Lidarr request flow)
|
|
# Update this number whenever you add/remove an @singleton.
|
|
assert len(_singleton_registry) == 59
|
|
|
|
def test_all_entries_have_cache_clear(self):
|
|
for fn in _singleton_registry:
|
|
assert hasattr(fn, "cache_clear"), f"{fn.__name__} missing cache_clear"
|
|
|
|
def test_clear_all_singletons_calls_cache_clear(self):
|
|
before = [fn.cache_info().currsize for fn in _singleton_registry]
|
|
clear_all_singletons()
|
|
after = [fn.cache_info().currsize for fn in _singleton_registry]
|
|
assert all(s == 0 for s in after)
|
|
|
|
|
|
class TestReExportCompleteness:
|
|
def test_init_exports_all_providers(self):
|
|
init = importlib.import_module("core.dependencies")
|
|
from core.dependencies import cache_providers, repo_providers, service_providers
|
|
|
|
for mod in (cache_providers, repo_providers, service_providers):
|
|
for name in dir(mod):
|
|
obj = getattr(mod, name)
|
|
if name.startswith("get_") and getattr(obj, "__module__", "") == mod.__name__:
|
|
assert hasattr(init, name), f"{name} not re-exported from __init__"
|
|
|
|
def test_init_exports_all_type_aliases(self):
|
|
init = importlib.import_module("core.dependencies")
|
|
from core.dependencies import type_aliases
|
|
|
|
for name in dir(type_aliases):
|
|
if name.endswith("Dep"):
|
|
assert hasattr(init, name), f"{name} not re-exported from __init__"
|
|
|
|
def test_init_exports_cleanup_functions(self):
|
|
from core.dependencies import (
|
|
init_app_state,
|
|
cleanup_app_state,
|
|
clear_lastfm_dependent_caches,
|
|
clear_listenbrainz_dependent_caches,
|
|
clear_all_singletons,
|
|
)
|
|
assert callable(init_app_state)
|
|
assert callable(cleanup_app_state)
|
|
assert callable(clear_lastfm_dependent_caches)
|
|
assert callable(clear_listenbrainz_dependent_caches)
|
|
assert callable(clear_all_singletons)
|
|
|
|
|
|
class TestSingletonDecorator:
|
|
def test_singleton_caches_return_value(self):
|
|
from core.dependencies._registry import singleton
|
|
|
|
call_count = 0
|
|
|
|
@singleton
|
|
def my_provider():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
return object()
|
|
|
|
a = my_provider()
|
|
b = my_provider()
|
|
assert a is b
|
|
assert call_count == 1
|
|
|
|
my_provider.cache_clear()
|
|
c = my_provider()
|
|
assert c is not a
|
|
assert call_count == 2
|
|
|
|
# clean up: remove from registry
|
|
_singleton_registry.remove(my_provider)
|