* feat(persistence): add AlbumRepository.GetYears for distinct album years
* test(persistence): verify GetYears de-duplicates repeated years
Regression test that adds two albums with the same non-zero max_year
(2005) and verifies that GetYears() returns that year exactly once,
ensuring the SQL DISTINCT clause is applied correctly. Catches any
future removal of DISTINCT from the GetYears query.
* feat(jellyfin): add legacy /Items/Filters endpoint (genres + years)
* feat(jellyfin): add /Studios endpoint from record label tags
* refactor(persistence): drop duplicate columns in tagRepository.GetAll
* fix(jellyfin): exclude missing albums from filter years
GetYears only filtered max_year > 0, so albums whose files were all removed
(missing=true, kept when Scanner.PurgeMissing=never) contributed stale years
to /Items/Filters. Filter them out like the normal album listings do. Also
return an empty slice from the MockAlbumRepo to match the real repository.
* feat(jellyfin): filter /Items by Years=
* feat(jellyfin): filter /Items by StudioIds= (record labels)
* feat(jellyfin): scope filter and studio lists to ParentId library
* refactor(jellyfin): extract parentIDScope and libraryScopeFilter helpers
Collapse the three inline resolveLibraryScope(dto.DecodeID(parentid)) call
sites and the duplicated empty-scope guard into two small helpers, so the
empty-scope=unrestricted contract lives in one place. Reuse the existing
names() helper in the Years= e2e test.
* feat(jellyfin): expose record labels as album Studios
Add a Studios field to the album BaseItemDto, populated from the record-label
tags and gated behind Fields=Studios (matching Jellyfin's ItemFields
convention). Studio ids reuse the record-label tag identity, so they round-trip
with the /Studios list and the StudioIds= filter. Real Jellyfin leaves Studios
empty for music; Feishin reads it as the album's record label.