Add per-artist metadata source override for watchlist scans
Users can now override which metadata provider (Spotify, Deezer, Apple Music, Discogs) is used when scanning a specific watchlist artist for new releases. The selector appears in the artist config modal and only shows sources the artist has enrichment IDs for. Default behavior is unchanged — all artists use the global metadata source unless explicitly overridden.
This commit is contained in:
parent
f749bb9604
commit
b17a6e2dd7
6 changed files with 125 additions and 5 deletions
|
|
@ -865,7 +865,14 @@ class WatchlistScanner:
|
|||
Returns:
|
||||
WatchlistDiscographyResult or None on error
|
||||
"""
|
||||
for source in self._watchlist_source_priority():
|
||||
# Per-artist metadata source override — if set, use that source first with fallback
|
||||
preferred = getattr(watchlist_artist, 'preferred_metadata_source', None)
|
||||
if preferred and preferred in ('spotify', 'deezer', 'itunes', 'discogs'):
|
||||
source_priority = list(get_source_priority(preferred))
|
||||
else:
|
||||
source_priority = self._watchlist_source_priority()
|
||||
|
||||
for source in source_priority:
|
||||
result = self._resolve_watchlist_discography_for_source(watchlist_artist, source, last_scan_timestamp)
|
||||
if result:
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ class WatchlistArtist:
|
|||
include_compilations: bool = False
|
||||
include_instrumentals: bool = False
|
||||
lookback_days: Optional[int] = None # Per-artist override; None = use global setting
|
||||
preferred_metadata_source: Optional[str] = None # Per-artist override; None = use global setting
|
||||
profile_id: int = 1
|
||||
|
||||
@dataclass
|
||||
|
|
@ -378,6 +379,9 @@ class MusicDatabase:
|
|||
# Add iTunes artist ID column to watchlist_artists (migration)
|
||||
self._add_watchlist_itunes_id_column(cursor)
|
||||
|
||||
# Add per-artist preferred_metadata_source column (migration)
|
||||
self._add_watchlist_preferred_metadata_source_column(cursor)
|
||||
|
||||
# Make spotify_artist_id nullable for iTunes-only artists (migration)
|
||||
self._fix_watchlist_spotify_id_nullable(cursor)
|
||||
|
||||
|
|
@ -1529,6 +1533,17 @@ class MusicDatabase:
|
|||
logger.error(f"Error adding itunes_artist_id column to watchlist_artists: {e}")
|
||||
# Don't raise - this is a migration, database can still function
|
||||
|
||||
def _add_watchlist_preferred_metadata_source_column(self, cursor):
|
||||
"""Add per-artist preferred_metadata_source column to watchlist_artists table"""
|
||||
try:
|
||||
cursor.execute("PRAGMA table_info(watchlist_artists)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
if 'preferred_metadata_source' not in columns:
|
||||
cursor.execute("ALTER TABLE watchlist_artists ADD COLUMN preferred_metadata_source TEXT DEFAULT NULL")
|
||||
logger.info("Added preferred_metadata_source column to watchlist_artists table")
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding preferred_metadata_source column to watchlist_artists: {e}")
|
||||
|
||||
def _add_similar_artists_last_featured_column(self, cursor):
|
||||
"""Add last_featured column to similar_artists for hero slider cycling"""
|
||||
try:
|
||||
|
|
@ -6941,7 +6956,7 @@ class MusicDatabase:
|
|||
'last_scan_timestamp', 'created_at', 'updated_at']
|
||||
optional_columns = ['image_url', 'itunes_artist_id', 'deezer_artist_id', 'discogs_artist_id', 'include_albums', 'include_eps', 'include_singles',
|
||||
'include_live', 'include_remixes', 'include_acoustic', 'include_compilations',
|
||||
'include_instrumentals', 'lookback_days']
|
||||
'include_instrumentals', 'lookback_days', 'preferred_metadata_source']
|
||||
|
||||
columns_to_select = base_columns + [col for col in optional_columns if col in existing_columns]
|
||||
|
||||
|
|
@ -6977,6 +6992,7 @@ class MusicDatabase:
|
|||
include_compilations = bool(row['include_compilations']) if 'include_compilations' in existing_columns else False
|
||||
include_instrumentals = bool(row['include_instrumentals']) if 'include_instrumentals' in existing_columns else False
|
||||
lookback_days = row['lookback_days'] if 'lookback_days' in existing_columns else None
|
||||
preferred_metadata_source = row['preferred_metadata_source'] if 'preferred_metadata_source' in existing_columns else None
|
||||
|
||||
watchlist_artists.append(WatchlistArtist(
|
||||
id=row['id'],
|
||||
|
|
@ -6999,6 +7015,7 @@ class MusicDatabase:
|
|||
include_compilations=include_compilations,
|
||||
include_instrumentals=include_instrumentals,
|
||||
lookback_days=lookback_days,
|
||||
preferred_metadata_source=preferred_metadata_source,
|
||||
profile_id=profile_id
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -40957,7 +40957,7 @@ 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
|
||||
lookback_days, discogs_artist_id, preferred_metadata_source
|
||||
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))
|
||||
|
|
@ -41063,8 +41063,10 @@ def watchlist_artist_config(artist_id):
|
|||
'last_scan_timestamp': result[11],
|
||||
'date_added': result[12],
|
||||
'lookback_days': result[15] if len(result) > 15 else None,
|
||||
'preferred_metadata_source': result[17] if len(result) > 17 else None,
|
||||
}
|
||||
|
||||
from core.metadata_service import get_primary_source
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"config": config,
|
||||
|
|
@ -41075,6 +41077,7 @@ def watchlist_artist_config(artist_id):
|
|||
"deezer_artist_id": deezer_id,
|
||||
"discogs_artist_id": discogs_id,
|
||||
"watchlist_name": result[7], # Original stored watchlist artist name
|
||||
"global_metadata_source": get_primary_source(),
|
||||
})
|
||||
|
||||
else: # POST
|
||||
|
|
@ -41094,6 +41097,10 @@ def watchlist_artist_config(artist_id):
|
|||
# Validate lookback_days if provided
|
||||
if lookback_days is not None:
|
||||
lookback_days = int(lookback_days) if lookback_days != '' else None
|
||||
preferred_metadata_source = data.get('preferred_metadata_source', None)
|
||||
# Validate — only accept known sources, empty string means clear override
|
||||
if preferred_metadata_source == '' or preferred_metadata_source not in ('spotify', 'deezer', 'itunes', 'discogs'):
|
||||
preferred_metadata_source = None
|
||||
|
||||
# Validate at least one release type is selected
|
||||
if not (include_albums or include_eps or include_singles):
|
||||
|
|
@ -41116,13 +41123,13 @@ def watchlist_artist_config(artist_id):
|
|||
UPDATE watchlist_artists
|
||||
SET include_albums = ?, include_eps = ?, include_singles = ?,
|
||||
include_live = ?, include_remixes = ?, include_acoustic = ?, include_compilations = ?,
|
||||
include_instrumentals = ?, lookback_days = ?,
|
||||
include_instrumentals = ?, lookback_days = ?, preferred_metadata_source = ?,
|
||||
last_scan_timestamp = CASE WHEN ? THEN NULL ELSE last_scan_timestamp END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ?
|
||||
""", (int(include_albums), int(include_eps), int(include_singles),
|
||||
int(include_live), int(include_remixes), int(include_acoustic), int(include_compilations),
|
||||
int(include_instrumentals), lookback_days, lookback_changed,
|
||||
int(include_instrumentals), lookback_days, preferred_metadata_source, lookback_changed,
|
||||
artist_id, artist_id, artist_id, artist_id))
|
||||
conn.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -7390,6 +7390,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Source Override -->
|
||||
<div class="config-section">
|
||||
<h3 class="config-section-title">Scan Source</h3>
|
||||
<p class="config-section-subtitle">Override which metadata provider is used when scanning this artist for new releases</p>
|
||||
<div class="config-metadata-source-selector" id="config-metadata-source-selector">
|
||||
<!-- Dynamically populated with source buttons -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Linked Provider Section -->
|
||||
<div class="config-section" id="watchlist-linked-provider-section" style="display:none">
|
||||
<h3 class="config-section-title">Linked Artist</h3>
|
||||
|
|
|
|||
|
|
@ -41672,6 +41672,43 @@ async function openWatchlistArtistConfigModal(artistId, artistName) {
|
|||
document.getElementById('config-include-instrumentals').checked = config.include_instrumentals || false;
|
||||
document.getElementById('config-lookback-days').value = config.lookback_days != null ? String(config.lookback_days) : '';
|
||||
|
||||
// Populate metadata source selector
|
||||
const sourceSelector = document.getElementById('config-metadata-source-selector');
|
||||
if (sourceSelector) {
|
||||
const sources = [
|
||||
{ key: 'spotify', label: 'Spotify', id: spotify_artist_id, color: '#1DB954' },
|
||||
{ key: 'deezer', label: 'Deezer', id: deezer_artist_id, color: '#A238FF' },
|
||||
{ key: 'itunes', label: 'Apple Music', id: itunes_artist_id, color: '#FC3C44' },
|
||||
{ key: 'discogs', label: 'Discogs', id: discogs_artist_id, color: '#333' },
|
||||
];
|
||||
const globalSource = data.global_metadata_source || 'deezer';
|
||||
const currentOverride = config.preferred_metadata_source;
|
||||
const globalLabel = { spotify: 'Spotify', deezer: 'Deezer', itunes: 'Apple Music', discogs: 'Discogs' }[globalSource] || globalSource;
|
||||
|
||||
let html = `<button class="config-msrc-btn ${!currentOverride ? 'active' : ''}" data-source="" title="Use global default (${globalLabel})">
|
||||
<span class="config-msrc-icon">🌐</span><span class="config-msrc-label">Default (${globalLabel})</span>
|
||||
</button>`;
|
||||
for (const src of sources) {
|
||||
if (!src.id) continue;
|
||||
const isActive = currentOverride === src.key;
|
||||
html += `<button class="config-msrc-btn ${isActive ? 'active' : ''}" data-source="${src.key}" style="${isActive ? 'border-color:' + src.color : ''}" title="${src.label}">
|
||||
<span class="config-msrc-label">${src.label}</span>
|
||||
</button>`;
|
||||
}
|
||||
sourceSelector.innerHTML = html;
|
||||
sourceSelector.querySelectorAll('.config-msrc-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
sourceSelector.querySelectorAll('.config-msrc-btn').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
b.style.borderColor = '';
|
||||
});
|
||||
btn.classList.add('active');
|
||||
const color = sources.find(s => s.key === btn.dataset.source)?.color;
|
||||
if (color) btn.style.borderColor = color;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show global override notice if active
|
||||
const existingNotice = document.querySelector('.global-override-notice');
|
||||
if (existingNotice) existingNotice.remove();
|
||||
|
|
@ -42128,6 +42165,8 @@ async function saveWatchlistArtistConfig(artistId) {
|
|||
const includeInstrumentals = document.getElementById('config-include-instrumentals').checked;
|
||||
const lookbackDaysVal = document.getElementById('config-lookback-days').value;
|
||||
const lookbackDays = lookbackDaysVal !== '' ? parseInt(lookbackDaysVal) : null;
|
||||
const activeSourceBtn = document.querySelector('#config-metadata-source-selector .config-msrc-btn.active');
|
||||
const preferredMetadataSource = activeSourceBtn ? (activeSourceBtn.dataset.source || null) : null;
|
||||
|
||||
// Validate at least one release type is selected
|
||||
if (!includeAlbums && !includeEps && !includeSingles) {
|
||||
|
|
@ -42156,6 +42195,7 @@ async function saveWatchlistArtistConfig(artistId) {
|
|||
include_compilations: includeCompilations,
|
||||
include_instrumentals: includeInstrumentals,
|
||||
lookback_days: lookbackDays,
|
||||
preferred_metadata_source: preferredMetadataSource,
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17138,6 +17138,46 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-metadata-source-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.config-msrc-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.config-msrc-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.config-msrc-btn.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--accent-color, #1DB954);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.config-msrc-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.config-msrc-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
Loading…
Reference in a new issue