From 6750c20dc407efdafb2ec908c0522ba8e61f1d90 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Thu, 29 Jan 2026 08:55:19 -0800 Subject: [PATCH] display all listenbrainz playlsts in unique categories. --- core/listenbrainz_manager.py | 12 +-- webui/static/script.js | 162 ++++++++++++++++++++++++++++++++--- 2 files changed, 155 insertions(+), 19 deletions(-) diff --git a/core/listenbrainz_manager.py b/core/listenbrainz_manager.py index 6180f376..bdbd53ad 100644 --- a/core/listenbrainz_manager.py +++ b/core/listenbrainz_manager.py @@ -285,21 +285,21 @@ class ListenBrainzManager: logger.info(f"✅ Fetched {covers_found}/{len(track_data_list)} cover art URLs") def _cleanup_old_playlists(self): - """Remove old playlists, keeping only the 4 most recent per type""" + """Remove old playlists, keeping only the 25 most recent per type""" conn = self._get_db_connection() cursor = conn.cursor() - # For each playlist type, keep only the 4 most recent + # For each playlist type, keep only the 25 most recent playlist_types = ['created_for', 'user', 'collaborative'] for playlist_type in playlist_types: try: - # Get IDs of playlists to delete (all except 4 most recent) + # Get IDs of playlists to delete (all except 25 most recent) cursor.execute(""" SELECT id FROM listenbrainz_playlists WHERE playlist_type = ? ORDER BY last_updated DESC - LIMIT -1 OFFSET 4 + LIMIT -1 OFFSET 25 """, (playlist_type,)) old_playlist_ids = [row[0] for row in cursor.fetchall()] @@ -330,7 +330,7 @@ class ListenBrainzManager: return count > 0 def get_cached_playlists(self, playlist_type: str) -> List[Dict]: - """Get cached playlists of a specific type from database (limited to 4 most recent)""" + """Get cached playlists of a specific type from database (up to 25 most recent)""" conn = self._get_db_connection() cursor = conn.cursor() @@ -339,7 +339,7 @@ class ListenBrainzManager: FROM listenbrainz_playlists WHERE playlist_type = ? ORDER BY last_updated DESC - LIMIT 4 + LIMIT 25 """, (playlist_type,)) playlists = [] diff --git a/webui/static/script.js b/webui/static/script.js index 1cb6434b..be4227f7 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -31502,6 +31502,7 @@ async function openDownloadModalForGenre(genreName) { let listenbrainzPlaylistsCache = {}; // Store playlists by type let listenbrainzTracksCache = {}; // Store tracks for each playlist let activeListenBrainzTab = 'recommendations'; // Track active tab +let activeListenBrainzSubTab = null; // Track active sub-tab within recommendations async function initializeListenBrainzTabs() { try { @@ -31622,17 +31623,47 @@ function switchListenBrainzTab(tabId) { loadListenBrainzTabContent(tabId); } -async function loadListenBrainzTabContent(tabId) { - const container = document.getElementById('listenbrainz-tab-content'); - if (!container) return; +function groupListenBrainzPlaylists(playlists) { + const groups = {}; + const groupOrder = []; - const playlists = listenbrainzPlaylistsCache[tabId] || []; - if (playlists.length === 0) { - container.innerHTML = '

No playlists in this category

'; - return; + playlists.forEach(playlist => { + const playlistData = playlist.playlist || playlist; + const title = (playlistData.title || '').toLowerCase(); + + let groupName; + if (title.includes('weekly jams')) { + groupName = 'Weekly Jams'; + } else if (title.includes('weekly exploration')) { + groupName = 'Weekly Exploration'; + } else if (title.includes('top discoveries')) { + groupName = 'Top Discoveries'; + } else if (title.includes('top missed recordings')) { + groupName = 'Top Missed Recordings'; + } else if (title.includes('daily jams')) { + groupName = 'Daily Jams'; + } else { + groupName = 'Other'; + } + + if (!groups[groupName]) { + groups[groupName] = []; + groupOrder.push(groupName); + } + groups[groupName].push(playlist); + }); + + // Move "Other" to the end if it exists + const otherIdx = groupOrder.indexOf('Other'); + if (otherIdx !== -1 && otherIdx !== groupOrder.length - 1) { + groupOrder.splice(otherIdx, 1); + groupOrder.push('Other'); } - // Build HTML for all playlists in this tab + return { groups, groupOrder }; +} + +function buildListenBrainzPlaylistsHtml(playlists, tabId) { let html = ''; playlists.forEach((playlist, index) => { const playlistData = playlist.playlist || playlist; @@ -31702,18 +31733,123 @@ async function loadListenBrainzTabContent(tabId) { `; }); + return html; +} - container.innerHTML = html; - - // Load tracks for all playlists in this tab - playlists.forEach((playlist, index) => { +function loadTracksForPlaylists(playlists) { + playlists.forEach((playlist) => { const playlistData = playlist.playlist || playlist; const identifier = playlistData.identifier?.split('/').pop() || ''; - const playlistId = `discover-lb-playlist-${identifier}`; // Use consistent MBID-based ID + const playlistId = `discover-lb-playlist-${identifier}`; loadListenBrainzPlaylistTracks(identifier, playlistId); }); } +function switchListenBrainzSubTab(groupId) { + activeListenBrainzSubTab = groupId; + + // Update sub-tab buttons + const subTabs = document.querySelectorAll('#lb-subtabs-bar .lb-subtab'); + subTabs.forEach(tab => { + if (tab.dataset.group === groupId) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + }); + + // Show/hide sub-tab content panels + const panels = document.querySelectorAll('.lb-subtab-panel'); + panels.forEach(panel => { + if (panel.dataset.group === groupId) { + panel.style.display = 'block'; + // Load tracks for playlists in this panel if not already loaded + const unloaded = panel.querySelectorAll('.discover-loading'); + if (unloaded.length > 0) { + const groupPlaylists = panel._playlists; + if (groupPlaylists) { + loadTracksForPlaylists(groupPlaylists); + } + } + } else { + panel.style.display = 'none'; + } + }); +} + +async function loadListenBrainzTabContent(tabId) { + const container = document.getElementById('listenbrainz-tab-content'); + if (!container) return; + + const playlists = listenbrainzPlaylistsCache[tabId] || []; + if (playlists.length === 0) { + container.innerHTML = '

No playlists in this category

'; + return; + } + + // For recommendations tab with multiple playlists, group into sub-tabs + if (tabId === 'recommendations' && playlists.length > 1) { + const { groups, groupOrder } = groupListenBrainzPlaylists(playlists); + + // If only one group, no need for sub-tabs + if (groupOrder.length <= 1) { + const html = buildListenBrainzPlaylistsHtml(playlists, tabId); + container.innerHTML = html; + loadTracksForPlaylists(playlists); + return; + } + + // Build sub-tabs bar + const firstGroup = activeListenBrainzSubTab && groupOrder.includes(activeListenBrainzSubTab) + ? activeListenBrainzSubTab + : groupOrder[0]; + activeListenBrainzSubTab = firstGroup; + + let subTabsHtml = '
'; + groupOrder.forEach(groupName => { + const isActive = groupName === firstGroup; + const count = groups[groupName].length; + subTabsHtml += ` + + `; + }); + subTabsHtml += '
'; + + // Build content panels for each group + let panelsHtml = ''; + groupOrder.forEach(groupName => { + const isActive = groupName === firstGroup; + panelsHtml += `
`; + panelsHtml += buildListenBrainzPlaylistsHtml(groups[groupName], tabId); + panelsHtml += '
'; + }); + + container.innerHTML = subTabsHtml + panelsHtml; + + // Store playlist references on panels for lazy loading + groupOrder.forEach(groupName => { + const panel = container.querySelector(`.lb-subtab-panel[data-group="${groupName}"]`); + if (panel) { + panel._playlists = groups[groupName]; + } + }); + + // Load tracks only for the active sub-tab + loadTracksForPlaylists(groups[firstGroup]); + return; + } + + // Default: flat list for user/collaborative tabs (or single-group recommendations) + const html = buildListenBrainzPlaylistsHtml(playlists, tabId); + container.innerHTML = html; + loadTracksForPlaylists(playlists); +} + async function loadListenBrainzPlaylistTracks(identifier, playlistId) { try { const playlistContainer = document.getElementById(`${playlistId}-playlist`);