Add per-artist watchlist lookback period override
New "Scan Lookback" dropdown in the watchlist artist config modal. Each artist can override the global lookback period (7d to entire discography). Default is "Use Global Setting" (NULL in DB). - Database: lookback_days INTEGER DEFAULT NULL on watchlist_artists, auto-migrated on startup - Scanner: checks per-artist lookback_days first, falls back to global discovery_lookback_period if NULL - Backend: GET/POST /api/watchlist/artist/<id>/config includes lookback_days. Changing lookback clears last_scan_timestamp to force a rescan with the new window - Frontend: dropdown with 8 options in artist config modal - Fully backwards compatible — existing artists unchanged
This commit is contained in:
parent
db104ec155
commit
a33f891fa6
5 changed files with 77 additions and 15 deletions
|
|
@ -502,7 +502,7 @@ class WatchlistScanner:
|
|||
logger.warning(f"No valid client/ID for {watchlist_artist.artist_name}")
|
||||
return None
|
||||
|
||||
albums = self._get_artist_discography_with_client(client, artist_id, last_scan_timestamp)
|
||||
albums = self._get_artist_discography_with_client(client, artist_id, last_scan_timestamp, lookback_days=watchlist_artist.lookback_days)
|
||||
|
||||
# If primary provider returned nothing, try the other provider as fallback
|
||||
if not albums:
|
||||
|
|
@ -533,7 +533,7 @@ class WatchlistScanner:
|
|||
|
||||
if fallback_client and fallback_id:
|
||||
logger.info(f"{provider.capitalize()} returned no albums for {watchlist_artist.artist_name}, falling back to {'iTunes' if provider == 'spotify' else 'Spotify'}")
|
||||
albums = self._get_artist_discography_with_client(fallback_client, fallback_id, last_scan_timestamp)
|
||||
albums = self._get_artist_discography_with_client(fallback_client, fallback_id, last_scan_timestamp, lookback_days=watchlist_artist.lookback_days)
|
||||
|
||||
return albums
|
||||
|
||||
|
|
@ -725,7 +725,7 @@ class WatchlistScanner:
|
|||
logger.warning(f"Could not update artist image for {watchlist_artist.artist_name}: {img_error}")
|
||||
|
||||
# Get artist discography using active provider
|
||||
albums = self._get_artist_discography_with_client(client, artist_id, watchlist_artist.last_scan_timestamp)
|
||||
albums = self._get_artist_discography_with_client(client, artist_id, watchlist_artist.last_scan_timestamp, lookback_days=watchlist_artist.lookback_days)
|
||||
|
||||
if albums is None:
|
||||
return ScanResult(
|
||||
|
|
@ -863,14 +863,19 @@ class WatchlistScanner:
|
|||
# Determine cutoff date for filtering
|
||||
cutoff_timestamp = last_scan_timestamp
|
||||
|
||||
# If no last scan timestamp, use lookback period setting
|
||||
# If no last scan timestamp, use per-artist lookback or global setting
|
||||
if cutoff_timestamp is None:
|
||||
lookback_period = self._get_lookback_period_setting()
|
||||
if lookback_period != 'all':
|
||||
# Convert period to days and create cutoff date (use UTC)
|
||||
days = int(lookback_period)
|
||||
cutoff_timestamp = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
logger.info(f"Using lookback period: {lookback_period} days (cutoff: {cutoff_timestamp})")
|
||||
if lookback_days is not None:
|
||||
# Per-artist override
|
||||
cutoff_timestamp = datetime.now(timezone.utc) - timedelta(days=lookback_days)
|
||||
logger.info(f"Using per-artist lookback: {lookback_days} days (cutoff: {cutoff_timestamp})")
|
||||
else:
|
||||
# Global setting
|
||||
lookback_period = self._get_lookback_period_setting()
|
||||
if lookback_period != 'all':
|
||||
days = int(lookback_period)
|
||||
cutoff_timestamp = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
logger.info(f"Using global lookback period: {lookback_period} days (cutoff: {cutoff_timestamp})")
|
||||
|
||||
# Filter by release date if we have a cutoff timestamp
|
||||
if cutoff_timestamp:
|
||||
|
|
@ -889,7 +894,7 @@ class WatchlistScanner:
|
|||
logger.error(f"Error getting discography for artist {spotify_artist_id}: {e}")
|
||||
return None
|
||||
|
||||
def _get_artist_discography_with_client(self, client, artist_id: str, last_scan_timestamp: Optional[datetime] = None) -> Optional[List]:
|
||||
def _get_artist_discography_with_client(self, client, artist_id: str, last_scan_timestamp: Optional[datetime] = None, lookback_days: Optional[int] = None) -> Optional[List]:
|
||||
"""
|
||||
Get artist's discography using the specified client, optionally filtered by release date.
|
||||
|
||||
|
|
@ -898,6 +903,7 @@ class WatchlistScanner:
|
|||
artist_id: Artist ID for the given client
|
||||
last_scan_timestamp: Only return releases after this date (for incremental scans)
|
||||
If None, uses lookback period setting from database
|
||||
lookback_days: Per-artist override for lookback period (None = use global setting)
|
||||
"""
|
||||
try:
|
||||
# Get all artist albums (albums + singles)
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class WatchlistArtist:
|
|||
include_acoustic: bool = False
|
||||
include_compilations: bool = False
|
||||
include_instrumentals: bool = False
|
||||
lookback_days: Optional[int] = None # Per-artist override; None = use global setting
|
||||
profile_id: int = 1
|
||||
|
||||
@dataclass
|
||||
|
|
@ -305,6 +306,9 @@ class MusicDatabase:
|
|||
# Add content type filter columns to watchlist_artists (migration)
|
||||
self._add_watchlist_content_type_filters(cursor)
|
||||
|
||||
# Add per-artist lookback_days column to watchlist_artists (migration)
|
||||
self._add_watchlist_lookback_days_column(cursor)
|
||||
|
||||
# Add iTunes artist ID column to watchlist_artists (migration)
|
||||
self._add_watchlist_itunes_id_column(cursor)
|
||||
|
||||
|
|
@ -1168,6 +1172,17 @@ class MusicDatabase:
|
|||
logger.error(f"Error adding content type filter columns to watchlist_artists: {e}")
|
||||
# Don't raise - this is a migration, database can still function
|
||||
|
||||
def _add_watchlist_lookback_days_column(self, cursor):
|
||||
"""Add per-artist lookback_days column to watchlist_artists table"""
|
||||
try:
|
||||
cursor.execute("PRAGMA table_info(watchlist_artists)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
if 'lookback_days' not in columns:
|
||||
cursor.execute("ALTER TABLE watchlist_artists ADD COLUMN lookback_days INTEGER DEFAULT NULL")
|
||||
logger.info("Added lookback_days column to watchlist_artists table")
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding lookback_days column to watchlist_artists: {e}")
|
||||
|
||||
def _add_watchlist_itunes_id_column(self, cursor):
|
||||
"""Add iTunes artist ID column to watchlist_artists table for cross-provider support"""
|
||||
try:
|
||||
|
|
@ -6318,7 +6333,7 @@ class MusicDatabase:
|
|||
'last_scan_timestamp', 'created_at', 'updated_at']
|
||||
optional_columns = ['image_url', 'itunes_artist_id', 'deezer_artist_id', 'include_albums', 'include_eps', 'include_singles',
|
||||
'include_live', 'include_remixes', 'include_acoustic', 'include_compilations',
|
||||
'include_instrumentals']
|
||||
'include_instrumentals', 'lookback_days']
|
||||
|
||||
columns_to_select = base_columns + [col for col in optional_columns if col in existing_columns]
|
||||
|
||||
|
|
@ -6352,6 +6367,7 @@ class MusicDatabase:
|
|||
include_acoustic = bool(row['include_acoustic']) if 'include_acoustic' in existing_columns else False
|
||||
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
|
||||
|
||||
watchlist_artists.append(WatchlistArtist(
|
||||
id=row['id'],
|
||||
|
|
@ -6372,6 +6388,7 @@ class MusicDatabase:
|
|||
include_acoustic=include_acoustic,
|
||||
include_compilations=include_compilations,
|
||||
include_instrumentals=include_instrumentals,
|
||||
lookback_days=lookback_days,
|
||||
profile_id=profile_id
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -34538,7 +34538,8 @@ def watchlist_artist_config(artist_id):
|
|||
SELECT include_albums, include_eps, include_singles,
|
||||
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
|
||||
last_scan_timestamp, date_added, include_instrumentals, deezer_artist_id,
|
||||
lookback_days
|
||||
FROM watchlist_artists
|
||||
WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ?
|
||||
""", (artist_id, artist_id, artist_id))
|
||||
|
|
@ -34640,6 +34641,7 @@ def watchlist_artist_config(artist_id):
|
|||
'include_instrumentals': bool(result[13]) if result[13] is not None else False,
|
||||
'last_scan_timestamp': result[11],
|
||||
'date_added': result[12],
|
||||
'lookback_days': result[15] if len(result) > 15 else None,
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
|
|
@ -34666,6 +34668,10 @@ def watchlist_artist_config(artist_id):
|
|||
include_acoustic = data.get('include_acoustic', False)
|
||||
include_compilations = data.get('include_compilations', False)
|
||||
include_instrumentals = data.get('include_instrumentals', False)
|
||||
lookback_days = data.get('lookback_days', None) # None = use global setting
|
||||
# Validate lookback_days if provided
|
||||
if lookback_days is not None:
|
||||
lookback_days = int(lookback_days) if lookback_days != '' else None
|
||||
|
||||
# Validate at least one release type is selected
|
||||
if not (include_albums or include_eps or include_singles):
|
||||
|
|
@ -34674,16 +34680,27 @@ def watchlist_artist_config(artist_id):
|
|||
# Update database
|
||||
conn = sqlite3.connect(str(database.database_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if lookback_days changed — if so, clear last_scan_timestamp to force rescan
|
||||
cursor.execute("""
|
||||
SELECT lookback_days FROM watchlist_artists
|
||||
WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ?
|
||||
""", (artist_id, artist_id, artist_id))
|
||||
old_row = cursor.fetchone()
|
||||
old_lookback = old_row[0] if old_row else None
|
||||
lookback_changed = old_lookback != lookback_days
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE watchlist_artists
|
||||
SET include_albums = ?, include_eps = ?, include_singles = ?,
|
||||
include_live = ?, include_remixes = ?, include_acoustic = ?, include_compilations = ?,
|
||||
include_instrumentals = ?,
|
||||
include_instrumentals = ?, lookback_days = ?,
|
||||
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 = ?
|
||||
""", (int(include_albums), int(include_eps), int(include_singles),
|
||||
int(include_live), int(include_remixes), int(include_acoustic), int(include_compilations),
|
||||
int(include_instrumentals),
|
||||
int(include_instrumentals), lookback_days, lookback_changed,
|
||||
artist_id, artist_id, artist_id))
|
||||
conn.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -5925,6 +5925,24 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h3 class="config-section-title">Scan Lookback</h3>
|
||||
<p class="config-section-subtitle">How far back to look for releases on first scan of this artist</p>
|
||||
<div class="form-group" style="margin: 12px 0 0;">
|
||||
<select id="config-lookback-days" class="form-select">
|
||||
<option value="">Use Global Setting</option>
|
||||
<option value="7">Last 7 days</option>
|
||||
<option value="30">Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
<option value="180">Last 6 months</option>
|
||||
<option value="365">Last year</option>
|
||||
<option value="730">Last 2 years</option>
|
||||
<option value="1825">Last 5 years</option>
|
||||
<option value="36500">Entire discography</option>
|
||||
</select>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -36986,6 +36986,7 @@ async function openWatchlistArtistConfigModal(artistId, artistName) {
|
|||
document.getElementById('config-include-acoustic').checked = config.include_acoustic || false;
|
||||
document.getElementById('config-include-compilations').checked = config.include_compilations || false;
|
||||
document.getElementById('config-include-instrumentals').checked = config.include_instrumentals || false;
|
||||
document.getElementById('config-lookback-days').value = config.lookback_days != null ? String(config.lookback_days) : '';
|
||||
|
||||
// Show global override notice if active
|
||||
const existingNotice = document.querySelector('.global-override-notice');
|
||||
|
|
@ -37443,6 +37444,8 @@ async function saveWatchlistArtistConfig(artistId) {
|
|||
const includeAcoustic = document.getElementById('config-include-acoustic').checked;
|
||||
const includeCompilations = document.getElementById('config-include-compilations').checked;
|
||||
const includeInstrumentals = document.getElementById('config-include-instrumentals').checked;
|
||||
const lookbackDaysVal = document.getElementById('config-lookback-days').value;
|
||||
const lookbackDays = lookbackDaysVal !== '' ? parseInt(lookbackDaysVal) : null;
|
||||
|
||||
// Validate at least one release type is selected
|
||||
if (!includeAlbums && !includeEps && !includeSingles) {
|
||||
|
|
@ -37470,6 +37473,7 @@ async function saveWatchlistArtistConfig(artistId) {
|
|||
include_acoustic: includeAcoustic,
|
||||
include_compilations: includeCompilations,
|
||||
include_instrumentals: includeInstrumentals,
|
||||
lookback_days: lookbackDays,
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue