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:
parent
1f6edbb1da
commit
42a833fcb2
11 changed files with 92 additions and 24 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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, '"')}"
|
||||
|
|
@ -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, '"')}"
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue