Amazon Music: UI badges, enrichment match chips, watchlist linking, metadata cache

- Artist cards, hero section, and enhanced view now show Amazon Music badges
  when amazon_id is populated (AMAZON_LOGO_URL constant, orange #FF9900 brand)
- Enhanced view artist and album match status rows include amazon_match_status
  chip with click-to-rematch via openManualMatchModal
- getServiceUrl: added amazon (album/track ASIN → music.amazon.com) and fixed
  missing discogs entries; serviceLabels adds tidal/qobuz/amazon
- Enhanced view enhanced-artist-id-badges includes amazon_id entry
- DB SELECTs for library artists list and artist detail now return amazon_id;
  both response dicts include the field
- watchlist_artists migration adds amazon_artist_id column
- Watchlist config GET: amazon_artist_id in SELECT/WHERE/response (index 18)
- Watchlist artists list response includes amazon_artist_id
- link-provider endpoint: amazon added to valid_providers and col_map
- _populateLinkedProviderSection: amazonId param + Amazon Music source row
- Watchlist card source badges render Amazon pill (watchlist-source-amazon CSS)
- _openSourceSearch labels map includes amazon
- service_search: amazon_worker injected via init(); _search_service amazon branch
  uses search_artists/albums/tracks, same {id,name,image,extra} return shape
- _SERVICE_ID_COLUMNS: amazon → amazon_id for artist/album/track
- _init_service_search call passes amazon_worker_obj
- amazon_client._fetch_album_metas: 5-minute TTL cache per ASIN — cached hits
  skip _rate_limit() and HTTP call entirely; fixes ~10s artist detail load
- registry.py: removed amazon from METADATA_SOURCE_PRIORITY and
  METADATA_SOURCE_LABELS — T2Tunes has no discography API, cannot serve as a
  primary metadata source; Amazon remains a download source + ASIN enricher
- Settings metadata source dropdown and help text updated accordingly
This commit is contained in:
Broque Thomas 2026-05-16 22:52:27 -07:00
parent 1f6edbb1da
commit 42a833fcb2
11 changed files with 92 additions and 24 deletions

View file

@ -64,6 +64,10 @@ MIN_API_INTERVAL = 0.5 # seconds — T2Tunes has no published rate limit
_last_api_call: float = 0.0
_api_call_lock = threading.Lock()
_META_CACHE_TTL = 300 # seconds
_meta_cache: Dict[str, tuple] = {} # asin -> (fetched_at, meta_dict)
_meta_cache_lock = threading.Lock()
class AmazonClientError(RuntimeError):
"""Raised on unrecoverable T2Tunes API errors."""
@ -657,12 +661,21 @@ class AmazonClient:
metas: Dict[str, Dict[str, Any]] = {}
def _fetch(asin: str) -> None:
now = time.monotonic()
with _meta_cache_lock:
entry = _meta_cache.get(asin)
if entry and (now - entry[0]) < _META_CACHE_TTL:
metas[asin] = entry[1]
return
_rate_limit()
try:
raw = self.album_metadata(asin)
lst = raw.get("albumList")
if isinstance(lst, list) and lst and isinstance(lst[0], dict):
metas[asin] = lst[0]
meta = lst[0]
with _meta_cache_lock:
_meta_cache[asin] = (time.monotonic(), meta)
metas[asin] = meta
except Exception:
pass

View file

@ -19,6 +19,7 @@ tidal_enrichment_worker = None
qobuz_enrichment_worker = None
discogs_worker = None
audiodb_worker = None
amazon_worker = None
def init(
@ -31,11 +32,12 @@ def init(
qobuz_worker=None,
discogs_worker_obj=None,
audiodb_worker_obj=None,
amazon_worker_obj=None,
):
"""Bind enrichment worker handles so the lifted bodies can use them."""
global spotify_enrichment_worker, itunes_enrichment_worker, mb_worker
global lastfm_worker, genius_worker, tidal_enrichment_worker
global qobuz_enrichment_worker, discogs_worker, audiodb_worker
global qobuz_enrichment_worker, discogs_worker, audiodb_worker, amazon_worker
spotify_enrichment_worker = spotify_worker
itunes_enrichment_worker = itunes_worker
mb_worker = musicbrainz_worker
@ -45,6 +47,7 @@ def init(
qobuz_enrichment_worker = qobuz_worker
discogs_worker = discogs_worker_obj
audiodb_worker = audiodb_worker_obj
amazon_worker = amazon_worker_obj
def _detect_provider(items, client):
@ -293,4 +296,22 @@ def _search_service(service, entity_type, query):
'image': None, 'extra': f"{result.get('strArtist', '')} · {result.get('strAlbum', '')}"}]
return []
elif service == 'amazon':
if not amazon_worker or not amazon_worker.client:
raise ValueError("Amazon worker not initialized")
client = amazon_worker.client
if entity_type == 'artist':
items = client.search_artists(query, limit=8)
return [{'id': str(a.id), 'name': a.name, 'image': a.image_url,
'extra': ', '.join(a.genres[:3]) if a.genres else ''} for a in items]
elif entity_type == 'album':
items = client.search_albums(query, limit=8)
return [{'id': str(a.id), 'name': a.name, 'image': a.image_url,
'extra': f"{', '.join(a.artists)} · {a.release_date or ''}"} for a in items]
elif entity_type == 'track':
items = client.search_tracks(query, limit=8)
return [{'id': str(t.id), 'name': t.name, 'image': t.image_url,
'extra': f"{', '.join(t.artists)} · {t.album or ''}"} for t in items]
return []
return []

View file

@ -18,14 +18,13 @@ logger = get_logger("metadata.registry")
MetadataClientFactory = Callable[[], Any]
METADATA_SOURCE_PRIORITY = ("deezer", "itunes", "spotify", "discogs", "hydrabase", "amazon")
METADATA_SOURCE_PRIORITY = ("deezer", "itunes", "spotify", "discogs", "hydrabase")
METADATA_SOURCE_LABELS = {
"spotify": "Spotify",
"itunes": "iTunes",
"deezer": "Deezer",
"discogs": "Discogs",
"hydrabase": "Hydrabase",
"amazon": "Amazon Music",
}
_UNSET = object()

View file

@ -1634,6 +1634,10 @@ class MusicDatabase:
cursor.execute("ALTER TABLE watchlist_artists ADD COLUMN discogs_artist_id TEXT")
logger.info("Added discogs_artist_id column to watchlist_artists table for cross-provider support")
if 'amazon_artist_id' not in columns:
cursor.execute("ALTER TABLE watchlist_artists ADD COLUMN amazon_artist_id TEXT")
logger.info("Added amazon_artist_id column to watchlist_artists table for Amazon Music support")
except Exception as e:
logger.error(f"Error adding itunes_artist_id column to watchlist_artists: {e}")
# Don't raise - this is a migration, database can still function
@ -8955,6 +8959,7 @@ class MusicDatabase:
a.tidal_id,
a.qobuz_id,
a.soul_id,
a.amazon_id,
a.server_source
FROM artists a
WHERE {where_clause}
@ -9054,6 +9059,7 @@ class MusicDatabase:
'tidal_id': row['tidal_id'],
'qobuz_id': row['qobuz_id'],
'soul_id': row['soul_id'],
'amazon_id': row['amazon_id'],
'album_count': counts_map.get(row['id'], (0, 0))[0],
'track_count': counts_map.get(row['id'], (0, 0))[1],
'is_watched': bool(is_watched)
@ -9112,7 +9118,7 @@ class MusicDatabase:
id, name, thumb_url, genres, server_source,
musicbrainz_id, deezer_id, audiodb_id, discogs_id,
spotify_artist_id, itunes_artist_id, lastfm_url, genius_url,
tidal_id, qobuz_id, soul_id,
tidal_id, qobuz_id, soul_id, amazon_id,
lastfm_listeners, lastfm_playcount, lastfm_tags, lastfm_bio
FROM artists
WHERE id = ?
@ -9270,6 +9276,7 @@ class MusicDatabase:
'tidal_id': artist_row['tidal_id'],
'qobuz_id': artist_row['qobuz_id'],
'soul_id': artist_row['soul_id'],
'amazon_id': artist_row['amazon_id'],
'lastfm_listeners': artist_row['lastfm_listeners'],
'lastfm_playcount': artist_row['lastfm_playcount'],
'lastfm_tags': artist_row['lastfm_tags'],

View file

@ -10655,6 +10655,7 @@ _SERVICE_ID_COLUMNS = {
'genius': {'artist': 'genius_id', 'track': 'genius_id'},
'tidal': {'artist': 'tidal_id', 'album': 'tidal_id', 'track': 'tidal_id'},
'qobuz': {'artist': 'qobuz_id', 'album': 'qobuz_id', 'track': 'qobuz_id'},
'amazon': {'artist': 'amazon_id', 'album': 'amazon_id', 'track': 'amazon_id'},
}
@app.route('/api/library/manual-match', methods=['PUT'])
@ -24372,6 +24373,7 @@ def get_watchlist_artists():
"itunes_artist_id": artist.itunes_artist_id, # For iTunes-only artists
"deezer_artist_id": getattr(artist, 'deezer_artist_id', None),
"discogs_artist_id": getattr(artist, 'discogs_artist_id', None),
"amazon_artist_id": getattr(artist, 'amazon_artist_id', None),
"include_albums": artist.include_albums,
"include_eps": artist.include_eps,
"include_singles": artist.include_singles,
@ -25171,10 +25173,10 @@ def watchlist_artist_config(artist_id):
include_live, include_remixes, include_acoustic, include_compilations,
artist_name, image_url, spotify_artist_id, itunes_artist_id,
last_scan_timestamp, date_added, include_instrumentals, deezer_artist_id,
lookback_days, discogs_artist_id, preferred_metadata_source
lookback_days, discogs_artist_id, preferred_metadata_source, amazon_artist_id
FROM watchlist_artists
WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ?
""", (artist_id, artist_id, artist_id, artist_id))
WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ? OR amazon_artist_id = ?
""", (artist_id, artist_id, artist_id, artist_id, artist_id))
result = cursor.fetchone()
conn.close()
@ -25183,10 +25185,11 @@ def watchlist_artist_config(artist_id):
# Determine if this is an iTunes or Spotify artist
is_itunes_artist = artist_id.isdigit()
spotify_id = result[9] # spotify_artist_id from query
spotify_id = result[9] # spotify_artist_id from query
itunes_id = result[10] # itunes_artist_id from query
deezer_id = result[14] # deezer_artist_id from query
discogs_id = result[16] # discogs_artist_id from query
amazon_id = result[18] if len(result) > 18 else None # amazon_artist_id from query
# Get artist info from Spotify (only for Spotify artists)
artist_info = None
@ -25294,6 +25297,7 @@ def watchlist_artist_config(artist_id):
"itunes_artist_id": itunes_id,
"deezer_artist_id": deezer_id,
"discogs_artist_id": discogs_id,
"amazon_artist_id": amazon_id,
"watchlist_name": result[7], # Original stored watchlist artist name
"global_metadata_source": get_primary_source(),
})
@ -25394,7 +25398,7 @@ def watchlist_artist_link_provider(artist_id):
new_provider_id = data.get('provider_id', '').strip()
provider = data.get('provider', '').strip()
valid_providers = ('spotify', 'itunes', 'deezer', 'discogs')
valid_providers = ('spotify', 'itunes', 'deezer', 'discogs', 'amazon')
if provider not in valid_providers:
return jsonify({"success": False, "error": f"Invalid provider. Must be one of: {', '.join(valid_providers)}"}), 400
@ -25408,8 +25412,8 @@ def watchlist_artist_link_provider(artist_id):
cursor.execute("""
SELECT id, artist_name, spotify_artist_id, itunes_artist_id
FROM watchlist_artists
WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ?
""", (artist_id, artist_id, artist_id, artist_id))
WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ? OR amazon_artist_id = ?
""", (artist_id, artist_id, artist_id, artist_id, artist_id))
row = cursor.fetchone()
if not row:
@ -25420,7 +25424,7 @@ def watchlist_artist_link_provider(artist_id):
artist_name = row[1]
# Check for duplicate — another watchlist artist already has this provider ID
col_map = {'spotify': 'spotify_artist_id', 'itunes': 'itunes_artist_id', 'deezer': 'deezer_artist_id', 'discogs': 'discogs_artist_id'}
col_map = {'spotify': 'spotify_artist_id', 'itunes': 'itunes_artist_id', 'deezer': 'deezer_artist_id', 'discogs': 'discogs_artist_id', 'amazon': 'amazon_artist_id'}
col = col_map[provider]
if not is_clear:
@ -32422,6 +32426,7 @@ _init_service_search(
qobuz_worker=qobuz_enrichment_worker,
discogs_worker_obj=discogs_worker,
audiodb_worker_obj=audiodb_worker,
amazon_worker_obj=amazon_worker,
)
# Qobuz status / pause / resume routes are now served by the

View file

@ -3655,11 +3655,10 @@
<option value="itunes">iTunes / Apple Music</option>
<option value="deezer">Deezer</option>
<option value="discogs">Discogs</option>
<option value="amazon">Amazon Music</option>
</select>
</div>
<div class="callback-info">
<div class="callback-help">Choose the primary source for artist, album, and track metadata. Spotify can only be selected while an active Spotify session exists. Discogs requires a personal token. Amazon Music uses the T2Tunes proxy — no account needed.</div>
<div class="callback-help">Choose the primary source for artist, album, and track metadata. Spotify can only be selected while an active Spotify session exists. Discogs requires a personal token.</div>
</div>
</div>

View file

@ -974,7 +974,8 @@ async function initializeWatchlistPage() {
if (artist.itunes_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-itunes">iTunes</span>');
if (artist.deezer_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-deezer">Deezer</span>');
if (artist.discogs_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-discogs">Discogs</span>');
const artistPrimaryId = artist.spotify_artist_id || artist.itunes_artist_id || artist.deezer_artist_id || artist.discogs_artist_id;
if (artist.amazon_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-amazon">Amazon</span>');
const artistPrimaryId = artist.spotify_artist_id || artist.itunes_artist_id || artist.deezer_artist_id || artist.discogs_artist_id || artist.amazon_artist_id;
return `
<div class="watchlist-artist-card"
data-artist-name="${artist.artist_name.toLowerCase().replace(/"/g, '&quot;')}"
@ -1766,7 +1767,8 @@ async function showWatchlistModal() {
if (artist.itunes_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-itunes">iTunes</span>');
if (artist.deezer_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-deezer">Deezer</span>');
if (artist.discogs_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-discogs">Discogs</span>');
const artistPrimaryId = artist.spotify_artist_id || artist.itunes_artist_id || artist.deezer_artist_id || artist.discogs_artist_id;
if (artist.amazon_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-amazon">Amazon</span>');
const artistPrimaryId = artist.spotify_artist_id || artist.itunes_artist_id || artist.deezer_artist_id || artist.discogs_artist_id || artist.amazon_artist_id;
return `
<div class="watchlist-artist-card"
data-artist-name="${artist.artist_name.toLowerCase().replace(/"/g, '&quot;')}"
@ -1922,7 +1924,7 @@ function closeWatchlistModal() {
* Populate the linked provider section in the watchlist config modal.
* Shows which Spotify/iTunes/Deezer artist is linked and allows changing it.
*/
function _populateLinkedProviderSection(artistId, artistName, spotifyId, itunesId, artistInfo, deezerId, discogsId) {
function _populateLinkedProviderSection(artistId, artistName, spotifyId, itunesId, artistInfo, deezerId, discogsId, amazonId) {
const section = document.getElementById('watchlist-linked-provider-section');
const content = document.getElementById('watchlist-linked-provider-content');
if (!section || !content) return;
@ -1934,6 +1936,7 @@ function _populateLinkedProviderSection(artistId, artistName, spotifyId, itunesI
{ key: 'itunes', label: 'Apple Music', icon: '🔴', id: itunesId || '', color: '#fc3c44' },
{ key: 'deezer', label: 'Deezer', icon: '🟣', id: deezerId || '', color: '#a238ff' },
{ key: 'discogs', label: 'Discogs', icon: '🟤', id: discogsId || '', color: '#b08968' },
{ key: 'amazon', label: 'Amazon Music', icon: '🟠', id: amazonId || '', color: '#FF9900' },
];
let html = '<div class="wl-linked-sources">';
@ -1977,7 +1980,7 @@ function _populateLinkedProviderSection(artistId, artistName, spotifyId, itunesI
function _openSourceSearch(sourceKey, artistId, artistName) {
const panel = document.getElementById('wl-linked-search-panel');
if (!panel) return;
const labels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', discogs: 'Discogs' };
const labels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', discogs: 'Discogs', amazon: 'Amazon Music' };
document.getElementById('wl-linked-search-title').textContent = `Search ${labels[sourceKey] || sourceKey}`;
const input = document.getElementById('wl-linked-search-input');
input.value = artistName;
@ -2105,10 +2108,10 @@ async function openWatchlistArtistConfigModal(artistId, artistName) {
return;
}
const { config, artist, spotify_artist_id, itunes_artist_id, deezer_artist_id, discogs_artist_id, watchlist_name } = data;
const { config, artist, spotify_artist_id, itunes_artist_id, deezer_artist_id, discogs_artist_id, amazon_artist_id, watchlist_name } = data;
// Populate linked provider section (use DB watchlist_name for mismatch comparison)
_populateLinkedProviderSection(artistId, watchlist_name || artistName, spotify_artist_id, itunes_artist_id, artist, deezer_artist_id, discogs_artist_id);
_populateLinkedProviderSection(artistId, watchlist_name || artistName, spotify_artist_id, itunes_artist_id, artist, deezer_artist_id, discogs_artist_id, amazon_artist_id);
// Check if global override is active
let globalOverrideActive = false;

View file

@ -676,6 +676,7 @@ const GENIUS_LOGO_URL = 'https://images.genius.com/8ed669cadd956443e29c70361ec4f
const TIDAL_LOGO_URL = 'https://www.svgrepo.com/show/519734/tidal.svg';
const QOBUZ_LOGO_URL = 'https://www.svgrepo.com/show/504778/qobuz.svg';
const DISCOGS_LOGO_URL = 'https://www.svgrepo.com/show/305957/discogs.svg';
const AMAZON_LOGO_URL = '/static/amazon.svg';
function getAudioDBLogoURL() { const el = document.querySelector('img.audiodb-logo'); return el ? el.src : null; }
// --- Wishlist Modal Persistence State Management ---

View file

@ -3415,10 +3415,11 @@ function closeHelperSearch() {
const WHATS_NEW = {
'2.5.3': [
{ unreleased: true },
{ title: 'Amazon Music Metadata Source', desc: 'Amazon Music is now a selectable primary metadata source alongside Spotify, iTunes, Deezer, and Discogs. backed by the same T2Tunes proxy as the download source — no account needed. covers track search, album lookup with cover art, and artist discography. select it under Settings → Connections → Metadata Source.', page: 'settings' },
{ title: 'Amazon Music Download Source', desc: 'new download source backed by T2Tunes proxy. searches the Amazon Music catalog, downloads 24-bit/48kHz FLAC (or Opus 320kbps / Dolby Atmos EAC3 fallback). codec waterfall mirrors Tidal/Qobuz — best quality first, auto-fallback. selectable as a standalone or hybrid source from Settings.', page: 'settings' },
{ title: 'Amazon Music Search Quality', desc: 'search results now show album art, artist images (album cover stand-in, same as iTunes), and correct track/disc numbers. feat. credits stripped from artist names so the same artist does not show as duplicates. [Explicit] stripped from album names so MusicBrainz matching works cleanly — Clean / Edited / Censored labels kept as-is. album clicks and artist detail pages now open instead of 404ing.', page: 'search' },
{ title: 'Amazon Music Enrichment Worker', desc: 'new background enrichment worker for Amazon Music, same as Deezer, iTunes, Spotify, Qobuz, and Tidal. matches library artists, albums, and tracks to Amazon ASINs, backfills artist thumbnails from album covers. shows up in the enrichment panel and pauses automatically during library scans.', page: 'settings' },
{ title: 'Amazon Music Enrichment Worker', desc: 'background enrichment worker matches library artists, albums, and tracks to Amazon ASINs and backfills artist thumbnails from album covers. shows up in the enrichment panel with its own orb and rate-limit gauge. pauses automatically during library scans.', page: 'settings' },
{ title: 'Amazon Music Library Badges', desc: 'Amazon Music badges now appear on artist cards, the artist hero section, and the enhanced library view alongside Spotify, Deezer, Tidal, etc. match status chips in the enhanced view show Amazon enrichment progress for artists and albums with click-to-rematch. album and track ASINs link out to music.amazon.com.', page: 'library' },
{ title: 'Amazon Music Watchlist Linking', desc: 'watchlist linked provider section now includes Amazon Music. shows which Amazon Music artist slug is mapped to a watchlist entry, with match/fix/clear controls and live search backed by the T2Tunes catalog.', page: 'settings' },
],
'2.5.2': [
// --- May 13, 2026 — 2.5.2 release ---

View file

@ -259,6 +259,7 @@ function buildLibraryArtistCardHTML(artist, index) {
if (artist.tidal_id) badges.push({ logo: TIDAL_LOGO_URL, fb: 'TD', title: 'Tidal', url: `https://tidal.com/browse/artist/${artist.tidal_id}` });
if (artist.qobuz_id) badges.push({ logo: QOBUZ_LOGO_URL, fb: 'Qz', title: 'Qobuz', url: `https://www.qobuz.com/artist/${artist.qobuz_id}` });
if (artist.discogs_id) badges.push({ logo: DISCOGS_LOGO_URL, fb: 'DC', title: 'Discogs', url: `https://www.discogs.com/artist/${artist.discogs_id}` });
if (artist.amazon_id) badges.push({ logo: AMAZON_LOGO_URL, fb: 'AMZ', title: 'Amazon Music', url: null });
if (artist.soul_id && !String(artist.soul_id).startsWith('soul_unnamed_')) badges.push({ logo: '/static/trans2.png', fb: 'SS', title: `SoulID: ${artist.soul_id}`, url: null });
// Watchlist badge
@ -1128,6 +1129,7 @@ function updateArtistDetailPageHeaderWithData(artist) {
if (artist.tidal_id) badges.push(_hb(TIDAL_LOGO_URL, 'TD', 'Tidal', `https://tidal.com/browse/artist/${artist.tidal_id}`));
if (artist.qobuz_id) badges.push(_hb(QOBUZ_LOGO_URL, 'Qz', 'Qobuz', `https://www.qobuz.com/artist/${artist.qobuz_id}`));
if (artist.discogs_id) badges.push(_hb(DISCOGS_LOGO_URL, 'DC', 'Discogs', `https://www.discogs.com/artist/${artist.discogs_id}`));
if (artist.amazon_id) badges.push(_hb(AMAZON_LOGO_URL, 'AMZ', 'Amazon Music', null));
if (artist.soul_id && !String(artist.soul_id).startsWith('soul_unnamed_')) badges.push(_hb('/static/trans2.png', 'SS', `SoulID: ${artist.soul_id}`, null));
badgesContainer.innerHTML = badges.join('');
@ -2959,6 +2961,7 @@ function renderArtistMetaPanel(artist) {
{ key: 'genius_url', label: 'Genius', svc: 'genius' },
{ key: 'tidal_id', label: 'Tidal', svc: 'tidal' },
{ key: 'qobuz_id', label: 'Qobuz', svc: 'qobuz' },
{ key: 'amazon_id', label: 'Amazon Music', svc: 'amazon' },
];
idSources.forEach(src => {
if (artist[src.key]) {
@ -3094,6 +3097,7 @@ function renderArtistMetaPanel(artist) {
{ key: 'genius_match_status', label: 'Genius', attempted: 'genius_last_attempted', svc: 'genius' },
{ key: 'tidal_match_status', label: 'Tidal', attempted: 'tidal_last_attempted', svc: 'tidal' },
{ key: 'qobuz_match_status', label: 'Qobuz', attempted: 'qobuz_last_attempted', svc: 'qobuz' },
{ key: 'amazon_match_status', label: 'Amazon', attempted: 'amazon_last_attempted', svc: 'amazon' },
];
statusServices.forEach(s => {
const status = artist[s.key];
@ -3462,6 +3466,7 @@ function renderExpandedAlbumHeader(album) {
{ key: 'discogs_match_status', label: 'Discogs', attempted: 'discogs_last_attempted', svc: 'discogs' },
{ key: 'itunes_match_status', label: 'iTunes', attempted: 'itunes_last_attempted', svc: 'itunes' },
{ key: 'lastfm_match_status', label: 'Last.fm', attempted: 'lastfm_last_attempted', svc: 'lastfm' },
{ key: 'amazon_match_status', label: 'Amazon', attempted: 'amazon_last_attempted', svc: 'amazon' },
];
statusSvcs.forEach(s => {
const status = album[s.key];
@ -5125,6 +5130,14 @@ function getServiceUrl(service, entityType, id) {
album: `https://www.qobuz.com/album/${id}`,
track: `https://www.qobuz.com/track/${id}`,
},
discogs: {
artist: `https://www.discogs.com/artist/${id}`,
album: `https://www.discogs.com/release/${id}`,
},
amazon: {
album: `https://music.amazon.com/albums/${id}`,
track: `https://music.amazon.com/tracks/${id}`,
},
};
return urls[service] && urls[service][entityType] || null;
}
@ -5564,7 +5577,8 @@ function openManualMatchModal(entityType, entityId, service, defaultQuery, artis
const serviceLabels = {
spotify: 'Spotify', musicbrainz: 'MusicBrainz', deezer: 'Deezer',
audiodb: 'AudioDB', itunes: 'iTunes', lastfm: 'Last.fm', genius: 'Genius'
audiodb: 'AudioDB', itunes: 'iTunes', lastfm: 'Last.fm', genius: 'Genius',
tidal: 'Tidal', qobuz: 'Qobuz', amazon: 'Amazon Music'
};
const overlay = document.createElement('div');

View file

@ -16439,6 +16439,11 @@ body.helper-mode-active #dashboard-activity-feed:hover {
color: #D4A574;
}
.watchlist-source-amazon {
background: rgba(255, 153, 0, 0.15);
color: #FF9900;
}
.watchlist-card-pills {
position: relative;
z-index: 2;