Preserve Navidrome album cover art
Expose Navidrome album coverArt as a Subsonic getCoverArt thumbnail so library refreshes keep a real album-art URL. Preserve existing album thumb_url when an incoming server album has no thumbnail, preventing manual or server-corrected covers from being cleared and later replaced by loose missing-cover searches. Add regression tests for Navidrome album thumbnails and DB thumb preservation.
This commit is contained in:
parent
dad1b5109e
commit
96e6ba0ed7
4 changed files with 124 additions and 4 deletions
|
|
@ -67,6 +67,7 @@ class NavidromeAlbum:
|
|||
self.year = navidrome_data.get('year')
|
||||
self.addedAt = self._parse_date(navidrome_data.get('created'))
|
||||
self._artist_id = navidrome_data.get('artistId', '')
|
||||
self.thumb = self._get_album_image_url()
|
||||
|
||||
def _parse_date(self, date_str: Optional[str]) -> Optional[datetime]:
|
||||
if not date_str:
|
||||
|
|
@ -76,6 +77,18 @@ class NavidromeAlbum:
|
|||
except:
|
||||
return None
|
||||
|
||||
def _get_album_image_url(self) -> Optional[str]:
|
||||
"""Generate a Subsonic getCoverArt URL for this album.
|
||||
|
||||
Navidrome exposes the stable artwork key as ``coverArt``. Falling
|
||||
back to the album ID keeps compatibility with older responses while
|
||||
ensuring library refreshes do not mark albums as artless.
|
||||
"""
|
||||
cover_id = self._data.get('coverArt') or self.ratingKey
|
||||
if not cover_id:
|
||||
return None
|
||||
return f"/rest/getCoverArt?id={cover_id}"
|
||||
|
||||
def artist(self) -> Optional[NavidromeArtist]:
|
||||
"""Get the album artist"""
|
||||
if self._artist_id:
|
||||
|
|
@ -1242,4 +1255,4 @@ class NavidromeClient(MediaServerClient):
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching for tracks: {e}")
|
||||
return []
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -5247,7 +5247,7 @@ class MusicDatabase:
|
|||
# Album exists - update it (update server_source if different)
|
||||
cursor.execute("""
|
||||
UPDATE albums
|
||||
SET artist_id = ?, title = ?, year = ?, thumb_url = ?, genres = ?,
|
||||
SET artist_id = ?, title = ?, year = ?, thumb_url = COALESCE(NULLIF(?, ''), thumb_url), genres = ?,
|
||||
track_count = ?, duration = ?, server_source = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""", (artist_id, title, year, thumb_url, genres_json, track_count, duration, server_source, album_id))
|
||||
|
|
@ -5280,6 +5280,7 @@ class MusicDatabase:
|
|||
# Read enrichment data from old album
|
||||
cursor.execute("SELECT * FROM albums WHERE id = ?", (old_id,))
|
||||
old_row = cursor.fetchone()
|
||||
preserved_thumb_url = thumb_url or (old_row['thumb_url'] if old_row and 'thumb_url' in old_row.keys() else None)
|
||||
|
||||
# Insert new album with fresh server metadata + preserved created_at
|
||||
old_created = old_row['created_at'] if old_row else None
|
||||
|
|
@ -5287,7 +5288,7 @@ class MusicDatabase:
|
|||
INSERT INTO albums (id, artist_id, title, year, thumb_url, genres,
|
||||
track_count, duration, server_source, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
""", (album_id, artist_id, title, year, thumb_url, genres_json,
|
||||
""", (album_id, artist_id, title, year, preserved_thumb_url, genres_json,
|
||||
track_count, duration, server_source, old_created))
|
||||
|
||||
# Copy enrichment data from old record to new record
|
||||
|
|
|
|||
87
tests/database/test_album_thumb_preservation.py
Normal file
87
tests/database/test_album_thumb_preservation.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from database.music_database import MusicDatabase
|
||||
|
||||
|
||||
class _InMemoryDB(MusicDatabase):
|
||||
def __init__(self):
|
||||
self._conn = sqlite3.connect(":memory:")
|
||||
self._conn.row_factory = sqlite3.Row
|
||||
|
||||
def _get_connection(self):
|
||||
return _NonClosingConn(self._conn)
|
||||
|
||||
|
||||
class _NonClosingConn:
|
||||
def __init__(self, real):
|
||||
self._real = real
|
||||
|
||||
def cursor(self):
|
||||
return self._real.cursor()
|
||||
|
||||
def commit(self):
|
||||
return self._real.commit()
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class _Album:
|
||||
ratingKey = "album-1"
|
||||
title = "Flower Boy"
|
||||
year = 2017
|
||||
leafCount = 15
|
||||
duration = 2940
|
||||
genres = []
|
||||
thumb = None
|
||||
|
||||
|
||||
def _seed(db):
|
||||
cur = db._conn.cursor()
|
||||
cur.execute("""
|
||||
CREATE TABLE albums (
|
||||
id TEXT PRIMARY KEY,
|
||||
artist_id TEXT,
|
||||
title TEXT,
|
||||
year INTEGER,
|
||||
thumb_url TEXT,
|
||||
genres TEXT,
|
||||
track_count INTEGER,
|
||||
duration INTEGER,
|
||||
server_source TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
)
|
||||
""")
|
||||
cur.execute("""
|
||||
INSERT INTO albums
|
||||
(id, artist_id, title, year, thumb_url, server_source)
|
||||
VALUES
|
||||
('album-1', 'artist-1', 'Flower Boy', 2017, '/rest/getCoverArt?id=correct-cover', 'navidrome')
|
||||
""")
|
||||
db._conn.commit()
|
||||
|
||||
|
||||
def test_album_refresh_preserves_existing_thumb_when_incoming_thumb_missing():
|
||||
db = _InMemoryDB()
|
||||
_seed(db)
|
||||
|
||||
assert db.insert_or_update_media_album(_Album(), "artist-1", server_source="navidrome") is True
|
||||
|
||||
row = db._conn.execute("SELECT thumb_url FROM albums WHERE id = 'album-1'").fetchone()
|
||||
assert row["thumb_url"] == "/rest/getCoverArt?id=correct-cover"
|
||||
|
||||
|
||||
def test_album_refresh_updates_existing_thumb_when_incoming_thumb_present():
|
||||
db = _InMemoryDB()
|
||||
_seed(db)
|
||||
|
||||
album = _Album()
|
||||
album.thumb = "/rest/getCoverArt?id=new-cover"
|
||||
|
||||
assert db.insert_or_update_media_album(album, "artist-1", server_source="navidrome") is True
|
||||
|
||||
row = db._conn.execute("SELECT thumb_url FROM albums WHERE id = 'album-1'").fetchone()
|
||||
assert row["thumb_url"] == "/rest/getCoverArt?id=new-cover"
|
||||
|
|
@ -12,7 +12,7 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from core.navidrome_client import NavidromeClient
|
||||
from core.navidrome_client import NavidromeAlbum, NavidromeClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -77,3 +77,22 @@ def test_get_all_album_ids_returns_set(nav_client):
|
|||
|
||||
assert isinstance(result, set)
|
||||
assert result == {'nav-1', 'nav-2'}
|
||||
|
||||
|
||||
def test_navidrome_album_exposes_cover_art_url(nav_client):
|
||||
album = NavidromeAlbum({
|
||||
'id': 'album-1',
|
||||
'name': 'Flower Boy',
|
||||
'coverArt': 'cover-123',
|
||||
}, nav_client)
|
||||
|
||||
assert album.thumb == '/rest/getCoverArt?id=cover-123'
|
||||
|
||||
|
||||
def test_navidrome_album_cover_art_falls_back_to_album_id(nav_client):
|
||||
album = NavidromeAlbum({
|
||||
'id': 'album-1',
|
||||
'name': 'Flower Boy',
|
||||
}, nav_client)
|
||||
|
||||
assert album.thumb == '/rest/getCoverArt?id=album-1'
|
||||
|
|
|
|||
Loading…
Reference in a new issue