Add watchlist filter to library page
This commit is contained in:
parent
93d65b1ad3
commit
139b8530f4
5 changed files with 115 additions and 13 deletions
|
|
@ -4018,7 +4018,7 @@ class MusicDatabase:
|
||||||
'server_source': server_source
|
'server_source': server_source
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_library_artists(self, search_query: str = "", letter: str = "", page: int = 1, limit: int = 50) -> Dict[str, Any]:
|
def get_library_artists(self, search_query: str = "", letter: str = "", page: int = 1, limit: int = 50, watchlist_filter: str = "all") -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Get artists for the library page with search, filtering, and pagination
|
Get artists for the library page with search, filtering, and pagination
|
||||||
|
|
||||||
|
|
@ -4027,6 +4027,7 @@ class MusicDatabase:
|
||||||
letter: Filter by first letter (a-z, #, or "" for all)
|
letter: Filter by first letter (a-z, #, or "" for all)
|
||||||
page: Page number (1-based)
|
page: Page number (1-based)
|
||||||
limit: Number of results per page
|
limit: Number of results per page
|
||||||
|
watchlist_filter: Filter by watchlist status ("all", "watched", "unwatched")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing artists list, pagination info, and total count
|
Dict containing artists list, pagination info, and total count
|
||||||
|
|
@ -4062,7 +4063,39 @@ class MusicDatabase:
|
||||||
|
|
||||||
where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
|
where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
|
||||||
|
|
||||||
# Get total count (matching dashboard method)
|
# Pre-fetch watchlist data (small table, single fast query)
|
||||||
|
cursor.execute("SELECT spotify_artist_id, itunes_artist_id, LOWER(artist_name) as name_lower FROM watchlist_artists")
|
||||||
|
watchlist_rows = cursor.fetchall()
|
||||||
|
wl_spotify = {r['spotify_artist_id'] for r in watchlist_rows if r['spotify_artist_id']}
|
||||||
|
wl_itunes = {r['itunes_artist_id'] for r in watchlist_rows if r['itunes_artist_id']}
|
||||||
|
wl_names = {r['name_lower'] for r in watchlist_rows if r['name_lower']}
|
||||||
|
|
||||||
|
# Apply watchlist filter as WHERE conditions using IN clauses
|
||||||
|
if watchlist_filter in ("watched", "unwatched"):
|
||||||
|
match_parts = []
|
||||||
|
match_params = []
|
||||||
|
if wl_spotify:
|
||||||
|
match_parts.append(f"(a.spotify_artist_id IS NOT NULL AND a.spotify_artist_id IN ({','.join('?' * len(wl_spotify))}))")
|
||||||
|
match_params.extend(wl_spotify)
|
||||||
|
if wl_itunes:
|
||||||
|
match_parts.append(f"(a.itunes_artist_id IS NOT NULL AND a.itunes_artist_id IN ({','.join('?' * len(wl_itunes))}))")
|
||||||
|
match_params.extend(wl_itunes)
|
||||||
|
if wl_names:
|
||||||
|
match_parts.append(f"LOWER(a.name) IN ({','.join('?' * len(wl_names))})")
|
||||||
|
match_params.extend(wl_names)
|
||||||
|
|
||||||
|
if match_parts:
|
||||||
|
combined = ' OR '.join(match_parts)
|
||||||
|
if watchlist_filter == "watched":
|
||||||
|
where_clause += f" AND ({combined})"
|
||||||
|
else:
|
||||||
|
where_clause += f" AND NOT ({combined})"
|
||||||
|
params.extend(match_params)
|
||||||
|
elif watchlist_filter == "watched":
|
||||||
|
# Empty watchlist, no artists can match
|
||||||
|
where_clause += " AND 0"
|
||||||
|
|
||||||
|
# Get total count
|
||||||
count_query = f"""
|
count_query = f"""
|
||||||
SELECT COUNT(*) as total_count
|
SELECT COUNT(*) as total_count
|
||||||
FROM artists a
|
FROM artists a
|
||||||
|
|
@ -4081,17 +4114,13 @@ class MusicDatabase:
|
||||||
a.thumb_url,
|
a.thumb_url,
|
||||||
a.genres,
|
a.genres,
|
||||||
a.musicbrainz_id,
|
a.musicbrainz_id,
|
||||||
|
a.spotify_artist_id,
|
||||||
|
a.itunes_artist_id,
|
||||||
COUNT(DISTINCT al.id) as album_count,
|
COUNT(DISTINCT al.id) as album_count,
|
||||||
COUNT(DISTINCT t.id) as track_count,
|
COUNT(DISTINCT t.id) as track_count
|
||||||
MAX(CASE WHEN wa.id IS NOT NULL THEN 1 ELSE 0 END) as is_watched
|
|
||||||
FROM artists a
|
FROM artists a
|
||||||
LEFT JOIN albums al ON a.id = al.artist_id
|
LEFT JOIN albums al ON a.id = al.artist_id
|
||||||
LEFT JOIN tracks t ON al.id = t.album_id
|
LEFT JOIN tracks t ON al.id = t.album_id
|
||||||
LEFT JOIN watchlist_artists wa ON (
|
|
||||||
(a.spotify_artist_id IS NOT NULL AND a.spotify_artist_id = wa.spotify_artist_id)
|
|
||||||
OR (a.itunes_artist_id IS NOT NULL AND a.itunes_artist_id = wa.itunes_artist_id)
|
|
||||||
OR LOWER(a.name) = LOWER(wa.artist_name)
|
|
||||||
)
|
|
||||||
WHERE {where_clause}
|
WHERE {where_clause}
|
||||||
GROUP BY a.id, a.name, a.thumb_url, a.genres, a.musicbrainz_id
|
GROUP BY a.id, a.name, a.thumb_url, a.genres, a.musicbrainz_id
|
||||||
ORDER BY a.name COLLATE NOCASE
|
ORDER BY a.name COLLATE NOCASE
|
||||||
|
|
@ -4124,6 +4153,13 @@ class MusicDatabase:
|
||||||
genres=genres
|
genres=genres
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Determine watchlist status via set lookups
|
||||||
|
is_watched = (
|
||||||
|
(row['spotify_artist_id'] and row['spotify_artist_id'] in wl_spotify)
|
||||||
|
or (row['itunes_artist_id'] and row['itunes_artist_id'] in wl_itunes)
|
||||||
|
or (row['name'] and row['name'].lower() in wl_names)
|
||||||
|
)
|
||||||
|
|
||||||
# Add stats
|
# Add stats
|
||||||
artist_data = {
|
artist_data = {
|
||||||
'id': artist.id,
|
'id': artist.id,
|
||||||
|
|
@ -4133,7 +4169,7 @@ class MusicDatabase:
|
||||||
'musicbrainz_id': row['musicbrainz_id'],
|
'musicbrainz_id': row['musicbrainz_id'],
|
||||||
'album_count': row['album_count'] or 0,
|
'album_count': row['album_count'] or 0,
|
||||||
'track_count': row['track_count'] or 0,
|
'track_count': row['track_count'] or 0,
|
||||||
'is_watched': bool(row['is_watched'])
|
'is_watched': bool(is_watched)
|
||||||
}
|
}
|
||||||
artists.append(artist_data)
|
artists.append(artist_data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4850,6 +4850,7 @@ def get_library_artists():
|
||||||
letter = request.args.get('letter', 'all')
|
letter = request.args.get('letter', 'all')
|
||||||
page = int(request.args.get('page', 1))
|
page = int(request.args.get('page', 1))
|
||||||
limit = int(request.args.get('limit', 75))
|
limit = int(request.args.get('limit', 75))
|
||||||
|
watchlist_filter = request.args.get('watchlist', 'all')
|
||||||
|
|
||||||
# Get database instance
|
# Get database instance
|
||||||
database = get_database()
|
database = get_database()
|
||||||
|
|
@ -4859,7 +4860,8 @@ def get_library_artists():
|
||||||
search_query=search_query,
|
search_query=search_query,
|
||||||
letter=letter,
|
letter=letter,
|
||||||
page=page,
|
page=page,
|
||||||
limit=limit
|
limit=limit,
|
||||||
|
watchlist_filter=watchlist_filter
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fix image URLs for all artists
|
# Fix image URLs for all artists
|
||||||
|
|
|
||||||
|
|
@ -1712,6 +1712,13 @@
|
||||||
<div class="library-search-icon">🔍</div>
|
<div class="library-search-icon">🔍</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Watchlist Filter -->
|
||||||
|
<div class="watchlist-filter" id="watchlist-filter">
|
||||||
|
<button class="watchlist-filter-btn active" data-filter="all">All</button>
|
||||||
|
<button class="watchlist-filter-btn" data-filter="watched">Watched</button>
|
||||||
|
<button class="watchlist-filter-btn" data-filter="unwatched">Unwatched</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Alphabet Selector -->
|
<!-- Alphabet Selector -->
|
||||||
<div class="alphabet-selector" id="alphabet-selector">
|
<div class="alphabet-selector" id="alphabet-selector">
|
||||||
<div class="alphabet-selector-inner">
|
<div class="alphabet-selector-inner">
|
||||||
|
|
|
||||||
|
|
@ -25299,7 +25299,8 @@ const libraryPageState = {
|
||||||
currentLetter: "all",
|
currentLetter: "all",
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
limit: 75,
|
limit: 75,
|
||||||
debounceTimer: null
|
debounceTimer: null,
|
||||||
|
watchlistFilter: "all"
|
||||||
};
|
};
|
||||||
|
|
||||||
function initializeLibraryPage() {
|
function initializeLibraryPage() {
|
||||||
|
|
@ -25309,6 +25310,9 @@ function initializeLibraryPage() {
|
||||||
// Initialize search functionality
|
// Initialize search functionality
|
||||||
initializeLibrarySearch();
|
initializeLibrarySearch();
|
||||||
|
|
||||||
|
// Initialize watchlist filter
|
||||||
|
initializeWatchlistFilter();
|
||||||
|
|
||||||
// Initialize alphabet selector
|
// Initialize alphabet selector
|
||||||
initializeAlphabetSelector();
|
initializeAlphabetSelector();
|
||||||
|
|
||||||
|
|
@ -25358,6 +25362,25 @@ function initializeLibrarySearch() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializeWatchlistFilter() {
|
||||||
|
const filterButtons = document.querySelectorAll(".watchlist-filter-btn");
|
||||||
|
|
||||||
|
filterButtons.forEach(button => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const filter = button.getAttribute("data-filter");
|
||||||
|
|
||||||
|
// Update active state
|
||||||
|
filterButtons.forEach(btn => btn.classList.remove("active"));
|
||||||
|
button.classList.add("active");
|
||||||
|
|
||||||
|
// Update state and reload
|
||||||
|
libraryPageState.watchlistFilter = filter;
|
||||||
|
libraryPageState.currentPage = 1;
|
||||||
|
loadLibraryArtists();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initializeAlphabetSelector() {
|
function initializeAlphabetSelector() {
|
||||||
const alphabetButtons = document.querySelectorAll(".alphabet-btn");
|
const alphabetButtons = document.querySelectorAll(".alphabet-btn");
|
||||||
|
|
||||||
|
|
@ -25408,7 +25431,8 @@ async function loadLibraryArtists() {
|
||||||
search: libraryPageState.currentSearch,
|
search: libraryPageState.currentSearch,
|
||||||
letter: libraryPageState.currentLetter,
|
letter: libraryPageState.currentLetter,
|
||||||
page: libraryPageState.currentPage,
|
page: libraryPageState.currentPage,
|
||||||
limit: libraryPageState.limit
|
limit: libraryPageState.limit,
|
||||||
|
watchlist: libraryPageState.watchlistFilter
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch artists from API
|
// Fetch artists from API
|
||||||
|
|
|
||||||
|
|
@ -12620,6 +12620,39 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alphabet Selector - Apple Style Design */
|
/* Alphabet Selector - Apple Style Design */
|
||||||
|
/* Watchlist Filter */
|
||||||
|
.watchlist-filter {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-filter-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-filter-btn:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-filter-btn.active {
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(29, 185, 84, 0.5);
|
||||||
|
background: rgba(29, 185, 84, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.alphabet-selector {
|
.alphabet-selector {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue