Add instrumental filter & custom exclusion terms to watchlist content filters
This commit is contained in:
parent
9856ec6772
commit
a5e72cff05
6 changed files with 187 additions and 11 deletions
|
|
@ -234,6 +234,63 @@ def is_acoustic_version(track_name: str, album_name: str = "") -> bool:
|
|||
|
||||
return False
|
||||
|
||||
def is_instrumental_version(track_name: str, album_name: str = "") -> bool:
|
||||
"""
|
||||
Detect if a track is an instrumental version.
|
||||
|
||||
Args:
|
||||
track_name: Track name to check
|
||||
album_name: Album name to check (optional)
|
||||
|
||||
Returns:
|
||||
True if this is an instrumental version, False otherwise
|
||||
"""
|
||||
if not track_name:
|
||||
return False
|
||||
|
||||
text_to_check = f"{track_name} {album_name}".lower()
|
||||
|
||||
instrumental_patterns = [
|
||||
r'\binstrumental\b', # Instrumental, Instrumental Version
|
||||
r'\binst\.\b', # Inst. (abbreviation)
|
||||
r'\bkaraoke\b', # Karaoke version
|
||||
r'\bbacking track\b', # Backing Track
|
||||
]
|
||||
|
||||
for pattern in instrumental_patterns:
|
||||
if re.search(pattern, text_to_check, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def matches_custom_exclude_terms(track_name: str, album_name: str, exclude_terms: list) -> str:
|
||||
"""
|
||||
Check if a track or album name contains any user-defined exclusion terms.
|
||||
|
||||
Args:
|
||||
track_name: Track name to check
|
||||
album_name: Album name to check
|
||||
exclude_terms: List of terms to exclude (case-insensitive)
|
||||
|
||||
Returns:
|
||||
The matched term if found, empty string if no match
|
||||
"""
|
||||
if not exclude_terms:
|
||||
return ""
|
||||
|
||||
text_to_check = f"{track_name} {album_name}".lower()
|
||||
|
||||
for term in exclude_terms:
|
||||
term = term.strip().lower()
|
||||
if not term:
|
||||
continue
|
||||
if term in text_to_check:
|
||||
return term
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def is_compilation_album(album_name: str) -> bool:
|
||||
"""
|
||||
Detect if an album is a compilation/greatest hits album.
|
||||
|
|
@ -1161,6 +1218,7 @@ class WatchlistScanner:
|
|||
include_remixes = getattr(watchlist_artist, 'include_remixes', False)
|
||||
include_acoustic = getattr(watchlist_artist, 'include_acoustic', False)
|
||||
include_compilations = getattr(watchlist_artist, 'include_compilations', False)
|
||||
include_instrumentals = getattr(watchlist_artist, 'include_instrumentals', False)
|
||||
|
||||
# Check compilation albums (album-level filter)
|
||||
if not include_compilations:
|
||||
|
|
@ -1184,6 +1242,25 @@ class WatchlistScanner:
|
|||
logger.debug(f"Skipping acoustic version: {track_name}")
|
||||
return False
|
||||
|
||||
# Check instrumental versions
|
||||
if not include_instrumentals:
|
||||
if is_instrumental_version(track_name, album_name):
|
||||
logger.debug(f"Skipping instrumental version: {track_name}")
|
||||
return False
|
||||
|
||||
# Check custom exclusion terms
|
||||
try:
|
||||
from config.settings import config_manager as _cfg
|
||||
exclude_terms_str = _cfg.get('watchlist.exclude_terms', '')
|
||||
if exclude_terms_str:
|
||||
exclude_terms = [t.strip() for t in exclude_terms_str.split(',') if t.strip()]
|
||||
matched_term = matches_custom_exclude_terms(track_name, album_name, exclude_terms)
|
||||
if matched_term:
|
||||
logger.debug(f"Skipping track '{track_name}' — matched custom exclusion term: '{matched_term}'")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking custom exclusion terms: {e}")
|
||||
|
||||
# Track passes all filters
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ class WatchlistArtist:
|
|||
include_remixes: bool = False
|
||||
include_acoustic: bool = False
|
||||
include_compilations: bool = False
|
||||
include_instrumentals: bool = False
|
||||
profile_id: int = 1
|
||||
|
||||
@dataclass
|
||||
|
|
@ -1047,7 +1048,8 @@ class MusicDatabase:
|
|||
'include_live': ('INTEGER', '0'), # 0 = False (exclude live versions by default)
|
||||
'include_remixes': ('INTEGER', '0'), # 0 = False (exclude remixes by default)
|
||||
'include_acoustic': ('INTEGER', '0'), # 0 = False (exclude acoustic by default)
|
||||
'include_compilations': ('INTEGER', '0') # 0 = False (exclude compilations by default)
|
||||
'include_compilations': ('INTEGER', '0'), # 0 = False (exclude compilations by default)
|
||||
'include_instrumentals': ('INTEGER', '0') # 0 = False (exclude instrumentals by default)
|
||||
}
|
||||
|
||||
for column_name, (column_type, default_value) in columns_to_add.items():
|
||||
|
|
@ -5069,7 +5071,8 @@ class MusicDatabase:
|
|||
base_columns = ['id', 'spotify_artist_id', 'artist_name', 'date_added',
|
||||
'last_scan_timestamp', 'created_at', 'updated_at']
|
||||
optional_columns = ['image_url', 'itunes_artist_id', 'include_albums', 'include_eps', 'include_singles',
|
||||
'include_live', 'include_remixes', 'include_acoustic', 'include_compilations']
|
||||
'include_live', 'include_remixes', 'include_acoustic', 'include_compilations',
|
||||
'include_instrumentals']
|
||||
|
||||
columns_to_select = base_columns + [col for col in optional_columns if col in existing_columns]
|
||||
|
||||
|
|
@ -5101,6 +5104,7 @@ class MusicDatabase:
|
|||
include_remixes = bool(row['include_remixes']) if 'include_remixes' in existing_columns else False
|
||||
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
|
||||
|
||||
watchlist_artists.append(WatchlistArtist(
|
||||
id=row['id'],
|
||||
|
|
@ -5119,6 +5123,7 @@ class MusicDatabase:
|
|||
include_remixes=include_remixes,
|
||||
include_acoustic=include_acoustic,
|
||||
include_compilations=include_compilations,
|
||||
include_instrumentals=include_instrumentals,
|
||||
profile_id=profile_id
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -29012,7 +29012,7 @@ 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
|
||||
last_scan_timestamp, date_added, include_instrumentals
|
||||
FROM watchlist_artists
|
||||
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
|
||||
""", (artist_id, artist_id))
|
||||
|
|
@ -29110,6 +29110,7 @@ def watchlist_artist_config(artist_id):
|
|||
'include_remixes': bool(result[4]),
|
||||
'include_acoustic': bool(result[5]),
|
||||
'include_compilations': bool(result[6]),
|
||||
'include_instrumentals': bool(result[13]) if result[13] is not None else False,
|
||||
'last_scan_timestamp': result[11],
|
||||
'date_added': result[12],
|
||||
}
|
||||
|
|
@ -29136,6 +29137,7 @@ def watchlist_artist_config(artist_id):
|
|||
include_remixes = data.get('include_remixes', False)
|
||||
include_acoustic = data.get('include_acoustic', False)
|
||||
include_compilations = data.get('include_compilations', False)
|
||||
include_instrumentals = data.get('include_instrumentals', False)
|
||||
|
||||
# Validate at least one release type is selected
|
||||
if not (include_albums or include_eps or include_singles):
|
||||
|
|
@ -29148,10 +29150,12 @@ 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 = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE spotify_artist_id = ? OR itunes_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),
|
||||
artist_id, artist_id))
|
||||
conn.commit()
|
||||
|
||||
|
|
@ -29161,7 +29165,7 @@ def watchlist_artist_config(artist_id):
|
|||
|
||||
conn.close()
|
||||
|
||||
print(f"✅ Updated watchlist config for artist {artist_id}: albums={include_albums}, eps={include_eps}, singles={include_singles}, live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, compilations={include_compilations}")
|
||||
print(f"✅ Updated watchlist config for artist {artist_id}: albums={include_albums}, eps={include_eps}, singles={include_singles}, live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, compilations={include_compilations}, instrumentals={include_instrumentals}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
|
|
@ -29173,7 +29177,8 @@ def watchlist_artist_config(artist_id):
|
|||
'include_live': include_live,
|
||||
'include_remixes': include_remixes,
|
||||
'include_acoustic': include_acoustic,
|
||||
'include_compilations': include_compilations
|
||||
'include_compilations': include_compilations,
|
||||
'include_instrumentals': include_instrumentals,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -29265,6 +29270,8 @@ def watchlist_global_config():
|
|||
'include_remixes': config_manager.get('watchlist.global_include_remixes', False),
|
||||
'include_acoustic': config_manager.get('watchlist.global_include_acoustic', False),
|
||||
'include_compilations': config_manager.get('watchlist.global_include_compilations', False),
|
||||
'include_instrumentals': config_manager.get('watchlist.global_include_instrumentals', False),
|
||||
'exclude_terms': config_manager.get('watchlist.exclude_terms', ''),
|
||||
}
|
||||
return jsonify({"success": True, "config": config})
|
||||
|
||||
|
|
@ -29281,6 +29288,8 @@ def watchlist_global_config():
|
|||
include_remixes = data.get('include_remixes', False)
|
||||
include_acoustic = data.get('include_acoustic', False)
|
||||
include_compilations = data.get('include_compilations', False)
|
||||
include_instrumentals = data.get('include_instrumentals', False)
|
||||
exclude_terms = data.get('exclude_terms', '')
|
||||
|
||||
# When override is enabled, validate at least one release type
|
||||
if global_override_enabled and not (include_albums or include_eps or include_singles):
|
||||
|
|
@ -29294,11 +29303,14 @@ def watchlist_global_config():
|
|||
config_manager.set('watchlist.global_include_remixes', include_remixes)
|
||||
config_manager.set('watchlist.global_include_acoustic', include_acoustic)
|
||||
config_manager.set('watchlist.global_include_compilations', include_compilations)
|
||||
config_manager.set('watchlist.global_include_instrumentals', include_instrumentals)
|
||||
config_manager.set('watchlist.exclude_terms', exclude_terms)
|
||||
|
||||
print(f"✅ Updated global watchlist config: override={global_override_enabled}, "
|
||||
f"albums={include_albums}, eps={include_eps}, singles={include_singles}, "
|
||||
f"live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, "
|
||||
f"compilations={include_compilations}")
|
||||
f"compilations={include_compilations}, instrumentals={include_instrumentals}, "
|
||||
f"exclude_terms='{exclude_terms}'")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
|
|
@ -29312,6 +29324,8 @@ def watchlist_global_config():
|
|||
'include_remixes': include_remixes,
|
||||
'include_acoustic': include_acoustic,
|
||||
'include_compilations': include_compilations,
|
||||
'include_instrumentals': include_instrumentals,
|
||||
'exclude_terms': exclude_terms,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -29333,9 +29347,11 @@ def _apply_watchlist_global_overrides(watchlist_artists):
|
|||
g_remixes = config_manager.get('watchlist.global_include_remixes', False)
|
||||
g_acoustic = config_manager.get('watchlist.global_include_acoustic', False)
|
||||
g_compilations = config_manager.get('watchlist.global_include_compilations', False)
|
||||
g_instrumentals = config_manager.get('watchlist.global_include_instrumentals', False)
|
||||
print(f"🌐 [Watchlist] Global override is ACTIVE — applying to {len(watchlist_artists)} artists "
|
||||
f"(albums={g_albums}, eps={g_eps}, singles={g_singles}, live={g_live}, "
|
||||
f"remixes={g_remixes}, acoustic={g_acoustic}, compilations={g_compilations})")
|
||||
f"remixes={g_remixes}, acoustic={g_acoustic}, compilations={g_compilations}, "
|
||||
f"instrumentals={g_instrumentals})")
|
||||
for artist in watchlist_artists:
|
||||
artist.include_albums = g_albums
|
||||
artist.include_eps = g_eps
|
||||
|
|
@ -29344,6 +29360,7 @@ def _apply_watchlist_global_overrides(watchlist_artists):
|
|||
artist.include_remixes = g_remixes
|
||||
artist.include_acoustic = g_acoustic
|
||||
artist.include_compilations = g_compilations
|
||||
artist.include_instrumentals = g_instrumentals
|
||||
|
||||
def _update_similar_artists_worker():
|
||||
"""Background worker to update similar artists for all watchlist artists"""
|
||||
|
|
|
|||
|
|
@ -5073,6 +5073,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="config-option">
|
||||
<input type="checkbox" id="config-include-instrumentals">
|
||||
<div class="config-option-content">
|
||||
<div class="config-option-icon">🎹</div>
|
||||
<div class="config-option-text">
|
||||
<span class="config-option-title">Include Instrumentals</span>
|
||||
<span class="config-option-description">Check to include instrumental, karaoke,
|
||||
and backing track versions</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -5227,6 +5239,17 @@
|
|||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="config-option">
|
||||
<input type="checkbox" id="global-include-instrumentals">
|
||||
<div class="config-option-content">
|
||||
<div class="config-option-icon">🎹</div>
|
||||
<div class="config-option-text">
|
||||
<span class="config-option-title">Include Instrumentals</span>
|
||||
<span class="config-option-description">Instrumental, karaoke, and backing track versions</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -5239,12 +5262,23 @@
|
|||
<div class="config-option-text">
|
||||
<span class="config-option-title">Include Everything</span>
|
||||
<span class="config-option-description">Enable all release types AND content filters
|
||||
(live, remixes, acoustic, compilations)</span>
|
||||
(live, remixes, acoustic, compilations, instrumentals)</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Exclusion Terms (always visible, independent of global override) -->
|
||||
<div class="config-section">
|
||||
<h3 class="config-section-title">Custom Exclusion Terms</h3>
|
||||
<p class="config-section-subtitle">Comma-separated terms — tracks or albums matching any term will be skipped during scans</p>
|
||||
<div class="config-exclude-terms-container">
|
||||
<input type="text" id="global-exclude-terms" class="config-exclude-terms-input"
|
||||
placeholder="e.g. commentary, interlude, skit, demo, a cappella">
|
||||
<p class="config-exclude-terms-hint">Applied globally to all watchlist scans regardless of override setting. Case-insensitive substring matching.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -32302,6 +32302,7 @@ async function openWatchlistArtistConfigModal(artistId, artistName) {
|
|||
document.getElementById('config-include-remixes').checked = config.include_remixes || false;
|
||||
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;
|
||||
|
||||
// Show global override notice if active
|
||||
const existingNotice = document.querySelector('.global-override-notice');
|
||||
|
|
@ -32561,6 +32562,8 @@ async function openWatchlistGlobalSettingsModal() {
|
|||
document.getElementById('global-include-remixes').checked = config.include_remixes;
|
||||
document.getElementById('global-include-acoustic').checked = config.include_acoustic;
|
||||
document.getElementById('global-include-compilations').checked = config.include_compilations;
|
||||
document.getElementById('global-include-instrumentals').checked = config.include_instrumentals;
|
||||
document.getElementById('global-exclude-terms').value = config.exclude_terms || '';
|
||||
|
||||
// Sync "Include Everything" checkbox
|
||||
syncGlobalIncludeAllCheckbox();
|
||||
|
|
@ -32621,7 +32624,7 @@ function toggleGlobalIncludeAll() {
|
|||
const checked = document.getElementById('global-include-all').checked;
|
||||
['global-include-albums', 'global-include-eps', 'global-include-singles',
|
||||
'global-include-live', 'global-include-remixes', 'global-include-acoustic',
|
||||
'global-include-compilations'].forEach(id => {
|
||||
'global-include-compilations', 'global-include-instrumentals'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.checked = checked;
|
||||
});
|
||||
|
|
@ -32633,7 +32636,7 @@ function toggleGlobalIncludeAll() {
|
|||
function syncGlobalIncludeAllCheckbox() {
|
||||
const allIds = ['global-include-albums', 'global-include-eps', 'global-include-singles',
|
||||
'global-include-live', 'global-include-remixes', 'global-include-acoustic',
|
||||
'global-include-compilations'];
|
||||
'global-include-compilations', 'global-include-instrumentals'];
|
||||
const allChecked = allIds.every(id => {
|
||||
const el = document.getElementById(id);
|
||||
return el && el.checked;
|
||||
|
|
@ -32655,6 +32658,8 @@ async function saveWatchlistGlobalConfig() {
|
|||
const includeRemixes = document.getElementById('global-include-remixes').checked;
|
||||
const includeAcoustic = document.getElementById('global-include-acoustic').checked;
|
||||
const includeCompilations = document.getElementById('global-include-compilations').checked;
|
||||
const includeInstrumentals = document.getElementById('global-include-instrumentals').checked;
|
||||
const excludeTerms = (document.getElementById('global-exclude-terms').value || '').trim();
|
||||
|
||||
if (globalOverrideEnabled && !includeAlbums && !includeEps && !includeSingles) {
|
||||
showToast('Please select at least one release type', 'error');
|
||||
|
|
@ -32679,6 +32684,8 @@ async function saveWatchlistGlobalConfig() {
|
|||
include_remixes: includeRemixes,
|
||||
include_acoustic: includeAcoustic,
|
||||
include_compilations: includeCompilations,
|
||||
include_instrumentals: includeInstrumentals,
|
||||
exclude_terms: excludeTerms,
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -32722,6 +32729,7 @@ async function saveWatchlistArtistConfig(artistId) {
|
|||
const includeRemixes = document.getElementById('config-include-remixes').checked;
|
||||
const includeAcoustic = document.getElementById('config-include-acoustic').checked;
|
||||
const includeCompilations = document.getElementById('config-include-compilations').checked;
|
||||
const includeInstrumentals = document.getElementById('config-include-instrumentals').checked;
|
||||
|
||||
// Validate at least one release type is selected
|
||||
if (!includeAlbums && !includeEps && !includeSingles) {
|
||||
|
|
@ -32747,7 +32755,8 @@ async function saveWatchlistArtistConfig(artistId) {
|
|||
include_live: includeLive,
|
||||
include_remixes: includeRemixes,
|
||||
include_acoustic: includeAcoustic,
|
||||
include_compilations: includeCompilations
|
||||
include_compilations: includeCompilations,
|
||||
include_instrumentals: includeInstrumentals,
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13136,6 +13136,40 @@ body {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.config-exclude-terms-container {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.config-exclude-terms-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.config-exclude-terms-input:focus {
|
||||
border-color: rgba(var(--accent-rgb), 0.5);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.config-exclude-terms-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.config-exclude-terms-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.watchlist-artist-config-footer {
|
||||
padding: 20px 28px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
|
|
|
|||
Loading…
Reference in a new issue