Fix track ownership false positives, wing-it wishlist leak, debug counts
Track ownership: check-tracks endpoint now filters by album context when provided, preventing false "Found" when a track exists in a different album by the same artist (e.g. Thriller on HIStory). Wing-it wishlist: manual "Add to Wishlist" button now skips wing-it fallback tracks (wing_it_ ID prefix), matching the behavior of failed download and failed sync paths. Debug info: watchlist/wishlist/automation counts were always 0 because get_db() doesn't exist — fixed to get_database().
This commit is contained in:
parent
2f2e5ddbd0
commit
21ae328955
3 changed files with 63 additions and 24 deletions
|
|
@ -5712,21 +5712,21 @@ def get_debug_info():
|
|||
|
||||
# Watchlist count
|
||||
try:
|
||||
db = get_db()
|
||||
db = get_database()
|
||||
info['watchlist_count'] = db.get_watchlist_count()
|
||||
except Exception:
|
||||
info['watchlist_count'] = 0
|
||||
|
||||
# Wishlist pending count
|
||||
try:
|
||||
db = get_db()
|
||||
db = get_database()
|
||||
info['wishlist_count'] = db.get_wishlist_count()
|
||||
except Exception:
|
||||
info['wishlist_count'] = 0
|
||||
|
||||
# Automation count
|
||||
try:
|
||||
db = get_db()
|
||||
db = get_database()
|
||||
automations = db.get_automations()
|
||||
info['automations'] = {
|
||||
'total': len(automations),
|
||||
|
|
@ -12034,6 +12034,36 @@ def library_check_tracks():
|
|||
# Pre-compute normalized DB titles once (keep reference to db_track for metadata)
|
||||
db_title_entries = [(_normalize(t.title), _clean_title(t.title), t) for t in db_tracks]
|
||||
|
||||
# Album-aware matching: if album_name provided, prefer tracks from that album
|
||||
target_album = data.get('album_name', '')
|
||||
target_album_norm = _normalize(target_album) if target_album else ''
|
||||
|
||||
def _match_title(search_norm, search_clean, candidates):
|
||||
"""Find best matching track from a list of (norm, clean, db_track) candidates."""
|
||||
for db_norm, db_clean, db_track in candidates:
|
||||
if search_norm == db_norm or search_clean == db_clean:
|
||||
return db_track
|
||||
sim = max(
|
||||
SequenceMatcher(None, search_norm, db_norm).ratio(),
|
||||
SequenceMatcher(None, search_clean, db_clean).ratio()
|
||||
)
|
||||
if sim >= 0.7:
|
||||
return db_track
|
||||
return None
|
||||
|
||||
# Split DB tracks by album if album-aware matching is active
|
||||
album_entries = []
|
||||
other_entries = []
|
||||
if target_album_norm:
|
||||
for entry in db_title_entries:
|
||||
db_album = _normalize(getattr(entry[2], 'album_title', '') or '')
|
||||
if db_album and SequenceMatcher(None, target_album_norm, db_album).ratio() >= 0.7:
|
||||
album_entries.append(entry)
|
||||
else:
|
||||
other_entries.append(entry)
|
||||
else:
|
||||
other_entries = db_title_entries
|
||||
|
||||
owned_map = {}
|
||||
for track in tracks:
|
||||
track_name = track.get('name', '')
|
||||
|
|
@ -12042,21 +12072,14 @@ def library_check_tracks():
|
|||
|
||||
search_norm = _normalize(track_name)
|
||||
search_clean = _clean_title(track_name)
|
||||
matched_db_track = None
|
||||
|
||||
for db_norm, db_clean, db_track in db_title_entries:
|
||||
# Check normalized match first (fast path for exact/near-exact)
|
||||
if search_norm == db_norm or search_clean == db_clean:
|
||||
matched_db_track = db_track
|
||||
break
|
||||
# Fuzzy match: try both normalized and cleaned
|
||||
sim = max(
|
||||
SequenceMatcher(None, search_norm, db_norm).ratio(),
|
||||
SequenceMatcher(None, search_clean, db_clean).ratio()
|
||||
)
|
||||
if sim >= 0.7:
|
||||
matched_db_track = db_track
|
||||
break
|
||||
# When album context provided, only match within that album —
|
||||
# prevents false positives where "Thriller" on Album A shows as owned
|
||||
# because it exists on Album B. Without album context, search all tracks.
|
||||
if target_album_norm:
|
||||
matched_db_track = _match_title(search_norm, search_clean, album_entries)
|
||||
else:
|
||||
matched_db_track = _match_title(search_norm, search_clean, other_entries)
|
||||
|
||||
if matched_db_track:
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -3603,6 +3603,9 @@ const WHATS_NEW = {
|
|||
// --- April 19, 2026 ---
|
||||
{ date: 'April 19, 2026' },
|
||||
{ title: 'Fix Wishlist Albums Cycle Stuck at 1 Concurrent', desc: 'Auto-wishlist processing during the "albums" cycle was limited to 1 concurrent download even with higher configured settings. The max_concurrent=1 restriction is only needed for Soulseek folder-based album grabs, not individual wishlist track downloads. Albums cycle now uses the configured concurrency like singles' },
|
||||
{ title: 'Fix Track Ownership False Positives Across Albums', desc: 'Track ownership check on the artist detail page now filters by album context. Previously "Thriller" from Thriller 25 would show as owned on every Michael Jackson album containing a track called Thriller. Now only matches within the specific album being checked' },
|
||||
{ title: 'Fix Wing It Tracks Added to Wishlist via Button', desc: 'Wing It fallback tracks were skipped from wishlist on failed downloads but not when manually clicking "Add to Wishlist". Now consistently skipped across all paths' },
|
||||
{ title: 'Fix Debug Info Showing Zero Counts', desc: 'Copy Debug Info button showed 0 for watchlist, wishlist, and automation counts due to calling get_db() instead of get_database(). Silent NameError was caught by try/except' },
|
||||
{ title: 'Fix Album Track Lookup Hardcoded to Spotify', desc: 'Clicking an album on the Artists page to download tracks was hardcoded to use Spotify even when the user\'s primary metadata source was Deezer or iTunes. Now uses the configured primary source with Spotify as fallback' },
|
||||
{ title: 'Fix Wishlist Splitting Albums by Track Artist', desc: 'Adding a multi-artist album (like a soundtrack) to wishlist was creating separate entries per track artist instead of keeping all tracks under the album artist. Now uses the album-level artist context when available to keep tracks grouped correctly' },
|
||||
{ title: 'Fix Artist Search Case Sensitivity', desc: 'Artist search on the Artists page now normalizes all-lowercase queries to title case before hitting metadata APIs. Some APIs return fewer or no results for lowercase queries like "foreigner" vs "Foreigner"' },
|
||||
|
|
|
|||
|
|
@ -20127,16 +20127,18 @@ async function handleAddToWishlist() {
|
|||
* Fetches ownership from the backend, then updates the modal DOM in-place.
|
||||
* If all tracks are owned (Spotify metadata discrepancy), also fixes the source card.
|
||||
*/
|
||||
async function lazyLoadTrackOwnership(artistName, tracks, sourceCard) {
|
||||
async function lazyLoadTrackOwnership(artistName, tracks, sourceCard, albumName = null) {
|
||||
const myVersion = wishlistModalVersion;
|
||||
try {
|
||||
const checkBody = {
|
||||
artist_name: artistName,
|
||||
tracks: tracks.map(t => ({ name: t.name, track_number: t.track_number }))
|
||||
};
|
||||
if (albumName) checkBody.album_name = albumName;
|
||||
const resp = await fetch('/api/library/check-tracks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
artist_name: artistName,
|
||||
tracks: tracks.map(t => ({ name: t.name, track_number: t.track_number }))
|
||||
})
|
||||
body: JSON.stringify(checkBody)
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.success) return;
|
||||
|
|
@ -20371,8 +20373,18 @@ async function addModalTracksToWishlist(playlistId) {
|
|||
let errorCount = 0;
|
||||
|
||||
// Add each track to wishlist individually
|
||||
let wingItSkipped = 0;
|
||||
for (const track of tracks) {
|
||||
try {
|
||||
// Skip wing-it fallback tracks — they have no real metadata,
|
||||
// adding them to wishlist would just retry with raw data
|
||||
const trackId = track.id || '';
|
||||
if (String(trackId).startsWith('wing_it_')) {
|
||||
wingItSkipped++;
|
||||
console.log(`⏭️ Skipping wing-it track from wishlist: ${track.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Format artists field to match backend expectations
|
||||
let formattedArtists = track.artists;
|
||||
if (typeof track.artists === 'string') {
|
||||
|
|
@ -20475,9 +20487,10 @@ async function addModalTracksToWishlist(playlistId) {
|
|||
|
||||
// Show result toast
|
||||
if (successCount > 0) {
|
||||
const message = errorCount > 0
|
||||
let message = errorCount > 0
|
||||
? `Added ${successCount}/${tracks.length} tracks to wishlist (${errorCount} failed)`
|
||||
: `Added ${successCount} tracks to wishlist`;
|
||||
if (wingItSkipped > 0) message += ` (${wingItSkipped} wing-it skipped)`;
|
||||
showToast(message, 'success');
|
||||
|
||||
// Close the modal on success
|
||||
|
|
@ -45289,7 +45302,7 @@ function createReleaseCard(release) {
|
|||
await openAddToWishlistModal(albumData, currentArtist, data.tracks, albumType);
|
||||
|
||||
// Always lazy-load track ownership + metadata (non-blocking)
|
||||
lazyLoadTrackOwnership(currentArtist.name, data.tracks, card);
|
||||
lazyLoadTrackOwnership(currentArtist.name, data.tracks, card, albumData.name);
|
||||
|
||||
} catch (error) {
|
||||
hideLoadingOverlay();
|
||||
|
|
|
|||
Loading…
Reference in a new issue