start of library page

This commit is contained in:
Broque Thomas 2025-09-23 11:42:58 -07:00
parent 180413677c
commit 9485dfb03b
4 changed files with 954 additions and 11 deletions

View file

@ -2590,14 +2590,141 @@ def maintain_search_history():
print(f"Error maintaining search history: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/artists')
def get_artists():
# Placeholder: returns mock artist data
mock_artists = [
{"name": "Queen", "album_count": 15, "image": None},
{"name": "Led Zeppelin", "album_count": 9, "image": None}
]
return jsonify({"artists": mock_artists})
def fix_artist_image_url(thumb_url):
"""Convert localhost URLs to proper server URLs using config"""
if not thumb_url:
return None
try:
# Check if it's a localhost URL or relative path that needs fixing
needs_fixing = (
thumb_url.startswith('http://localhost:') or
thumb_url.startswith('https://localhost:') or
thumb_url.startswith('/library/') # Plex relative paths
)
if needs_fixing:
active_server = config_manager.get_active_media_server()
print(f"🔧 Fixing URL: {thumb_url}, Active server: {active_server}")
if active_server == 'plex':
plex_config = config_manager.get_plex_config()
plex_base_url = plex_config.get('base_url', '')
plex_token = plex_config.get('token', '')
print(f"🔧 Plex config - base_url: {plex_base_url}, token: {plex_token[:10]}...")
if plex_base_url and plex_token:
# Extract the path from URL
if thumb_url.startswith('/library/'):
# Already a path
path = thumb_url
else:
# Full localhost URL, extract path
from urllib.parse import urlparse
parsed = urlparse(thumb_url)
path = parsed.path
# Construct proper Plex URL with token
fixed_url = f"{plex_base_url.rstrip('/')}{path}?X-Plex-Token={plex_token}"
print(f"🔧 Fixed URL: {fixed_url}")
return fixed_url
# For other servers, we might need similar logic later
# Return original URL if no fixing needed/possible
return thumb_url
except Exception as e:
print(f"Error fixing image URL '{thumb_url}': {e}")
return thumb_url
@app.route('/api/library/artists')
def get_library_artists():
"""Get artists for the library page with search, filtering, and pagination"""
try:
# Get query parameters
search_query = request.args.get('search', '')
letter = request.args.get('letter', 'all')
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 75))
# Get database instance
database = get_database()
# Get artists from database
result = database.get_library_artists(
search_query=search_query,
letter=letter,
page=page,
limit=limit
)
# Fix image URLs for all artists
for artist in result['artists']:
if artist.get('image_url'):
artist['image_url'] = fix_artist_image_url(artist['image_url'])
return jsonify({
"success": True,
**result
})
except Exception as e:
print(f"❌ Error fetching library artists: {e}")
import traceback
traceback.print_exc()
return jsonify({
"success": False,
"error": str(e),
"artists": [],
"pagination": {
"page": 1,
"limit": 75,
"total_count": 0,
"total_pages": 0,
"has_prev": False,
"has_next": False
}
}), 500
@app.route('/api/library/debug-photos')
def debug_library_photos():
"""Debug endpoint to check artist photo URLs"""
try:
database = get_database()
with database._get_connection() as conn:
cursor = conn.cursor()
# Get first 10 artists with their photo URLs
cursor.execute("""
SELECT name, thumb_url, server_source
FROM artists
WHERE thumb_url IS NOT NULL AND thumb_url != ''
LIMIT 10
""")
artists_with_photos = cursor.fetchall()
# Get first 10 artists without photos
cursor.execute("""
SELECT name, thumb_url, server_source
FROM artists
WHERE thumb_url IS NULL OR thumb_url = ''
LIMIT 10
""")
artists_without_photos = cursor.fetchall()
return jsonify({
"artists_with_photos": [dict(row) for row in artists_with_photos],
"artists_without_photos": [dict(row) for row in artists_without_photos],
"total_with_photos": len(artists_with_photos),
"total_without_photos": len(artists_without_photos)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/artist/<artist_id>/discography', methods=['GET'])
def get_artist_discography(artist_id):

View file

@ -33,6 +33,10 @@
<span class="nav-icon">🎵</span>
<span class="nav-text">Artists</span>
</button>
<button class="nav-button" data-page="library">
<span class="nav-icon">📚</span>
<span class="nav-text">Library</span>
</button>
<button class="nav-button" data-page="settings">
<span class="nav-icon">⚙️</span>
<span class="nav-text">Settings</span>
@ -626,7 +630,105 @@
</div>
</div>
</div>
<!-- Library Page -->
<div class="page" id="library-page">
<div class="library-container">
<!-- Header -->
<div class="library-header">
<div class="library-header-content">
<h2 class="library-title">📚 Music Library</h2>
<p class="library-subtitle">Browse your complete music collection</p>
</div>
<div class="library-stats" id="library-stats">
<span class="library-stat">
<span class="stat-number" id="library-artist-count">0</span>
<span class="stat-label">Artists</span>
</span>
</div>
</div>
<!-- Search and Filters -->
<div class="library-controls">
<div class="library-search-container">
<input type="text"
id="library-search-input"
class="library-search-input"
placeholder="Search artists...">
<div class="library-search-icon">🔍</div>
</div>
<!-- Alphabet Selector -->
<div class="alphabet-selector" id="alphabet-selector">
<div class="alphabet-selector-inner">
<button class="alphabet-btn active" data-letter="all">All</button>
<button class="alphabet-btn" data-letter="a">A</button>
<button class="alphabet-btn" data-letter="b">B</button>
<button class="alphabet-btn" data-letter="c">C</button>
<button class="alphabet-btn" data-letter="d">D</button>
<button class="alphabet-btn" data-letter="e">E</button>
<button class="alphabet-btn" data-letter="f">F</button>
<button class="alphabet-btn" data-letter="g">G</button>
<button class="alphabet-btn" data-letter="h">H</button>
<button class="alphabet-btn" data-letter="i">I</button>
<button class="alphabet-btn" data-letter="j">J</button>
<button class="alphabet-btn" data-letter="k">K</button>
<button class="alphabet-btn" data-letter="l">L</button>
<button class="alphabet-btn" data-letter="m">M</button>
<button class="alphabet-btn" data-letter="n">N</button>
<button class="alphabet-btn" data-letter="o">O</button>
<button class="alphabet-btn" data-letter="p">P</button>
<button class="alphabet-btn" data-letter="q">Q</button>
<button class="alphabet-btn" data-letter="r">R</button>
<button class="alphabet-btn" data-letter="s">S</button>
<button class="alphabet-btn" data-letter="t">T</button>
<button class="alphabet-btn" data-letter="u">U</button>
<button class="alphabet-btn" data-letter="v">V</button>
<button class="alphabet-btn" data-letter="w">W</button>
<button class="alphabet-btn" data-letter="x">X</button>
<button class="alphabet-btn" data-letter="y">Y</button>
<button class="alphabet-btn" data-letter="z">Z</button>
<button class="alphabet-btn" data-letter="#">#</button>
</div>
</div>
</div>
<!-- Content Area -->
<div class="library-content">
<!-- Loading State -->
<div class="library-loading hidden" id="library-loading">
<div class="loading-spinner"></div>
<div class="loading-text">Loading artists...</div>
</div>
<!-- Artist Grid -->
<div class="library-artists-grid" id="library-artists-grid">
<!-- Artist cards will be populated here -->
</div>
<!-- Empty State -->
<div class="library-empty hidden" id="library-empty">
<div class="empty-icon">🎵</div>
<div class="empty-title">No artists found</div>
<div class="empty-subtitle">Try adjusting your search or filters</div>
</div>
<!-- Pagination -->
<div class="library-pagination hidden" id="library-pagination">
<button class="pagination-btn" id="prev-page-btn" disabled>
<span>← Previous</span>
</button>
<div class="pagination-info">
<span id="page-info">Page 1 of 1</span>
</div>
<button class="pagination-btn" id="next-page-btn" disabled>
<span>Next →</span>
</button>
</div>
</div>
</div>
</div>
<!-- Settings Page -->
<div class="page" id="settings-page">
<div class="page-header">

View file

@ -396,6 +396,15 @@ async function loadPageData(pageId) {
restoreArtistsPageState();
}
break;
case 'library':
// Initialize and load library data
if (!libraryPageState.isInitialized) {
initializeLibraryPage();
} else {
// Refresh data when returning to page
await loadLibraryArtists();
}
break;
case 'settings':
initializeSettings();
await loadSettingsData();
@ -13587,4 +13596,299 @@ function cleanupSyncPageLogs() {
}
// --- Global Cleanup on Page Unload ---
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed
// ===============================
// LIBRARY PAGE FUNCTIONALITY
// ===============================
// Library page state
const libraryPageState = {
isInitialized: false,
currentSearch: "",
currentLetter: "all",
currentPage: 1,
limit: 75,
debounceTimer: null
};
function initializeLibraryPage() {
console.log("🔧 Initializing Library page...");
try {
// Initialize search functionality
initializeLibrarySearch();
// Initialize alphabet selector
initializeAlphabetSelector();
// Initialize pagination
initializeLibraryPagination();
// Load initial data
loadLibraryArtists();
libraryPageState.isInitialized = true;
console.log("✅ Library page initialized successfully");
} catch (error) {
console.error("❌ Error initializing Library page:", error);
showToast("Failed to initialize Library page", "error");
}
}
function initializeLibrarySearch() {
const searchInput = document.getElementById("library-search-input");
if (!searchInput) return;
searchInput.addEventListener("input", (e) => {
const query = e.target.value.trim();
// Clear existing debounce timer
if (libraryPageState.debounceTimer) {
clearTimeout(libraryPageState.debounceTimer);
}
// Debounce search requests
libraryPageState.debounceTimer = setTimeout(() => {
libraryPageState.currentSearch = query;
libraryPageState.currentPage = 1; // Reset to first page
loadLibraryArtists();
}, 300);
});
// Clear search on Escape key
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
searchInput.value = "";
libraryPageState.currentSearch = "";
libraryPageState.currentPage = 1;
loadLibraryArtists();
}
});
}
function initializeAlphabetSelector() {
const alphabetButtons = document.querySelectorAll(".alphabet-btn");
alphabetButtons.forEach(button => {
button.addEventListener("click", () => {
const letter = button.getAttribute("data-letter");
// Update active state
alphabetButtons.forEach(btn => btn.classList.remove("active"));
button.classList.add("active");
// Update state and load data
libraryPageState.currentLetter = letter;
libraryPageState.currentPage = 1; // Reset to first page
loadLibraryArtists();
});
});
}
function initializeLibraryPagination() {
const prevBtn = document.getElementById("prev-page-btn");
const nextBtn = document.getElementById("next-page-btn");
if (prevBtn) {
prevBtn.addEventListener("click", () => {
if (libraryPageState.currentPage > 1) {
libraryPageState.currentPage--;
loadLibraryArtists();
}
});
}
if (nextBtn) {
nextBtn.addEventListener("click", () => {
libraryPageState.currentPage++;
loadLibraryArtists();
});
}
}
async function loadLibraryArtists() {
try {
// Show loading state
showLibraryLoading(true);
// Build query parameters
const params = new URLSearchParams({
search: libraryPageState.currentSearch,
letter: libraryPageState.currentLetter,
page: libraryPageState.currentPage,
limit: libraryPageState.limit
});
// Fetch artists from API
const response = await fetch(`/api/library/artists?${params}`);
const data = await response.json();
if (!data.success) {
throw new Error(data.error || "Failed to load artists");
}
// Update UI with artists
displayLibraryArtists(data.artists);
updateLibraryPagination(data.pagination);
updateLibraryStats(data.pagination.total_count);
// Hide loading state
showLibraryLoading(false);
// Show empty state if no artists
if (data.artists.length === 0) {
showLibraryEmpty(true);
} else {
showLibraryEmpty(false);
}
} catch (error) {
console.error("❌ Error loading library artists:", error);
showToast("Failed to load artists", "error");
showLibraryLoading(false);
showLibraryEmpty(true);
}
}
function displayLibraryArtists(artists) {
const grid = document.getElementById("library-artists-grid");
if (!grid) return;
// Clear existing content
grid.innerHTML = "";
// Create artist cards
artists.forEach(artist => {
const card = createLibraryArtistCard(artist);
grid.appendChild(card);
});
}
function createLibraryArtistCard(artist) {
const card = document.createElement("div");
card.className = "library-artist-card";
card.setAttribute("data-artist-id", artist.id);
// Create image element
const imageContainer = document.createElement("div");
imageContainer.className = "library-artist-image";
if (artist.image_url && artist.image_url.trim() !== "") {
const img = document.createElement("img");
img.src = artist.image_url;
img.alt = artist.name;
img.onerror = () => {
console.log(`Failed to load image for ${artist.name}: ${artist.image_url}`);
// Replace with fallback on error
imageContainer.innerHTML = `<div class="library-artist-image-fallback">🎵</div>`;
};
img.onload = () => {
console.log(`Successfully loaded image for ${artist.name}: ${artist.image_url}`);
};
imageContainer.appendChild(img);
} else {
console.log(`No image URL for ${artist.name}: '${artist.image_url}'`);
imageContainer.innerHTML = `<div class="library-artist-image-fallback">🎵</div>`;
}
// Create info section
const info = document.createElement("div");
info.className = "library-artist-info";
const name = document.createElement("h3");
name.className = "library-artist-name";
name.textContent = artist.name;
name.title = artist.name; // For tooltip on long names
const stats = document.createElement("div");
stats.className = "library-artist-stats";
if (artist.album_count > 0 || artist.track_count > 0) {
const albumStat = document.createElement("span");
albumStat.className = "library-artist-stat";
albumStat.textContent = `${artist.album_count} album${artist.album_count !== 1 ? "s" : ""}`;
const trackStat = document.createElement("span");
trackStat.className = "library-artist-stat";
trackStat.textContent = `${artist.track_count} track${artist.track_count !== 1 ? "s" : ""}`;
stats.appendChild(albumStat);
stats.appendChild(trackStat);
}
info.appendChild(name);
info.appendChild(stats);
// Assemble card
card.appendChild(imageContainer);
card.appendChild(info);
// Add click handler (for future functionality)
card.addEventListener("click", () => {
// TODO: Implement artist detail view
console.log(`Clicked on artist: ${artist.name}`);
showToast(`Artist details coming soon!`, "info");
});
return card;
}
function updateLibraryPagination(pagination) {
const prevBtn = document.getElementById("prev-page-btn");
const nextBtn = document.getElementById("next-page-btn");
const pageInfo = document.getElementById("page-info");
const paginationContainer = document.getElementById("library-pagination");
if (!paginationContainer) return;
// Update button states
if (prevBtn) {
prevBtn.disabled = !pagination.has_prev;
}
if (nextBtn) {
nextBtn.disabled = !pagination.has_next;
}
// Update page info
if (pageInfo) {
pageInfo.textContent = `Page ${pagination.page} of ${pagination.total_pages}`;
}
// Show/hide pagination based on total pages
if (pagination.total_pages > 1) {
paginationContainer.classList.remove("hidden");
} else {
paginationContainer.classList.add("hidden");
}
}
function updateLibraryStats(totalCount) {
const countElement = document.getElementById("library-artist-count");
if (countElement) {
countElement.textContent = totalCount;
}
}
function showLibraryLoading(show) {
const loadingElement = document.getElementById("library-loading");
if (loadingElement) {
if (show) {
loadingElement.classList.remove("hidden");
} else {
loadingElement.classList.add("hidden");
}
}
}
function showLibraryEmpty(show) {
const emptyElement = document.getElementById("library-empty");
if (emptyElement) {
if (show) {
emptyElement.classList.remove("hidden");
} else {
emptyElement.classList.add("hidden");
}
}
}

View file

@ -8459,4 +8459,414 @@ body {
#metadata-updater-card{
display: flex!important;
justify-content: space-between;
}
}
/* ===============================
LIBRARY PAGE STYLING
=============================== */
.library-container {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
gap: 20px;
}
/* Library Header */
.library-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.library-header-content {
flex: 1;
}
.library-title {
font-size: 28px;
font-weight: 700;
color: #ffffff;
margin: 0 0 8px 0;
background: linear-gradient(135deg, #1ed760, #1db954);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.library-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.7);
margin: 0;
font-weight: 500;
}
.library-stats {
display: flex;
gap: 24px;
}
.library-stat {
text-align: center;
}
.stat-number {
display: block;
font-size: 24px;
font-weight: 700;
color: #1ed760;
line-height: 1;
}
.stat-label {
display: block;
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 4px;
}
/* Library Controls */
.library-controls {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Search Container */
.library-search-container {
position: relative;
max-width: 500px;
}
.library-search-input {
width: 100%;
padding: 14px 20px 14px 50px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
font-size: 15px;
font-weight: 500;
transition: all 0.3s ease;
outline: none;
}
.library-search-input:focus {
border-color: rgba(29, 185, 84, 0.6);
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 16px rgba(29, 185, 84, 0.2);
}
.library-search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.library-search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
color: rgba(255, 255, 255, 0.6);
pointer-events: none;
}
/* Alphabet Selector */
.alphabet-selector {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
}
.alphabet-selector::-webkit-scrollbar {
display: none;
}
.alphabet-selector-inner {
display: flex;
gap: 4px;
padding: 8px 0;
min-width: max-content;
}
.alphabet-btn {
padding: 8px 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.7);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
min-width: 36px;
text-align: center;
}
.alphabet-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
.alphabet-btn.active {
background: linear-gradient(135deg, #1db954, #1ed760);
border-color: rgba(29, 185, 84, 0.4);
color: #000000;
font-weight: 700;
}
/* Library Content */
.library-content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* Artists Grid */
.library-artists-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
padding: 20px 0;
flex: 1;
}
/* Library Artist Card */
.library-artist-card {
background: linear-gradient(135deg,
rgba(26, 26, 26, 0.95) 0%,
rgba(18, 18, 18, 0.98) 100%);
backdrop-filter: blur(12px) saturate(1.1);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid rgba(255, 255, 255, 0.12);
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 12px;
position: relative;
overflow: hidden;
}
.library-artist-card:hover {
transform: translateY(-4px);
border-color: rgba(29, 185, 84, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
0 0 20px rgba(29, 185, 84, 0.1);
}
.library-artist-card:active {
transform: translateY(-2px);
}
/* Artist Image */
.library-artist-image {
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
position: relative;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 100%);
border: 2px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.library-artist-card:hover .library-artist-image {
border-color: rgba(29, 185, 84, 0.4);
box-shadow: 0 0 20px rgba(29, 185, 84, 0.2);
}
.library-artist-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.3s ease;
}
.library-artist-card:hover .library-artist-image img {
transform: scale(1.05);
}
/* Artist Image Fallback */
.library-artist-image-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: rgba(255, 255, 255, 0.4);
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.08) 0%,
rgba(255, 255, 255, 0.03) 100%);
}
/* Artist Info */
.library-artist-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-height: 0;
}
.library-artist-name {
font-size: 16px;
font-weight: 700;
color: #ffffff;
margin: 0;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.library-artist-stats {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.library-artist-stat {
white-space: nowrap;
}
/* Empty State */
.library-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 16px;
padding: 60px 20px;
}
.empty-icon {
font-size: 64px;
opacity: 0.3;
}
.empty-title {
font-size: 24px;
font-weight: 700;
color: rgba(255, 255, 255, 0.8);
}
.empty-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.5);
text-align: center;
max-width: 400px;
}
/* Pagination */
.library-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
padding: 20px 0;
margin-top: auto;
}
.pagination-btn {
padding: 10px 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.pagination-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
.pagination-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pagination-info {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
font-weight: 500;
}
/* Responsive Design */
@media (max-width: 1200px) {
.library-artists-grid {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
}
.library-artist-image {
width: 100px;
height: 100px;
}
}
@media (max-width: 768px) {
.library-container {
padding: 16px;
gap: 16px;
}
.library-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.library-stats {
align-self: stretch;
justify-content: center;
}
.library-artists-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.library-artist-card {
padding: 16px;
}
.library-artist-image {
width: 80px;
height: 80px;
}
.library-artist-name {
font-size: 14px;
}
.library-artist-stats {
font-size: 12px;
}
}