fix database issue

This commit is contained in:
Broque Thomas 2025-09-26 13:16:58 -07:00
parent 00cb285202
commit cf33e17060

View file

@ -196,7 +196,7 @@ class MusicDatabase:
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Create indexes for performance
cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_artist_id ON albums (artist_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_album_id ON tracks (album_id)")
@ -387,7 +387,7 @@ class MusicDatabase:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM artists")
cursor.execute("SELECT COUNT(DISTINCT name) FROM artists")
artist_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM albums")
@ -412,8 +412,8 @@ class MusicDatabase:
cursor = conn.cursor()
if server_source:
# Get counts for specific server
cursor.execute("SELECT COUNT(*) FROM artists WHERE server_source = ?", (server_source,))
# Get counts for specific server (deduplicate by name like general count)
cursor.execute("SELECT COUNT(DISTINCT name) FROM artists WHERE server_source = ?", (server_source,))
artist_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM albums WHERE server_source = ?", (server_source,))
@ -555,7 +555,13 @@ class MusicDatabase:
# Convert artist ID to string (handles both Plex integer IDs and Jellyfin GUIDs)
artist_id = str(artist_obj.ratingKey)
name = artist_obj.title
raw_name = artist_obj.title
# Normalize artist name to handle quote variations and other inconsistencies
name = self._normalize_artist_name(raw_name)
# Debug logging to see if normalization is working
if raw_name != name:
logger.info(f"Artist name normalized: '{raw_name}' -> '{name}'")
thumb_url = getattr(artist_obj, 'thumb', None)
# Only preserve timestamps and flags from summary, not full biography
@ -593,23 +599,44 @@ class MusicDatabase:
if exists:
# Update existing artist
cursor.execute("""
UPDATE artists
UPDATE artists
SET name = ?, thumb_url = ?, genres = ?, summary = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND server_source = ?
""", (name, thumb_url, genres_json, summary, artist_id, server_source))
logger.debug(f"Updated existing {server_source} artist: {name} (ID: {artist_id})")
else:
# Insert new artist
cursor.execute("""
INSERT INTO artists (id, name, thumb_url, genres, summary, server_source)
VALUES (?, ?, ?, ?, ?, ?)
""", (artist_id, name, thumb_url, genres_json, summary, server_source))
logger.debug(f"Inserted new {server_source} artist: {name} (ID: {artist_id})")
conn.commit()
rows_affected = cursor.rowcount
if rows_affected == 0:
logger.warning(f"Database insertion returned 0 rows affected for {server_source} artist: {name} (ID: {artist_id})")
return True
except Exception as e:
logger.error(f"Error inserting/updating {server_source} artist {getattr(artist_obj, 'title', 'Unknown')}: {e}")
return False
def _normalize_artist_name(self, name: str) -> str:
"""
Normalize artist names to handle inconsistencies like quote variations.
Converts Unicode smart quotes to ASCII quotes for consistency.
"""
if not name:
return name
# Replace Unicode smart quotes with regular ASCII quotes
normalized = name.replace('\u201c', '"').replace('\u201d', '"') # Left and right double quotes
normalized = normalized.replace('\u2018', "'").replace('\u2019', "'") # Left and right single quotes
normalized = normalized.replace('\u00ab', '"').replace('\u00bb', '"') # « » guillemets
return normalized
def get_artist(self, artist_id: int) -> Optional[DatabaseArtist]:
"""Get artist by ID"""
@ -2219,6 +2246,305 @@ class MusicDatabase:
'server_source': server_source
}
def get_library_artists(self, search_query: str = "", letter: str = "", page: int = 1, limit: int = 50) -> Dict[str, Any]:
"""
Get artists for the library page with search, filtering, and pagination
Args:
search_query: Search term to filter artists by name
letter: Filter by first letter (a-z, #, or "" for all)
page: Page number (1-based)
limit: Number of results per page
Returns:
Dict containing artists list, pagination info, and total count
"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
# Build WHERE clause
where_conditions = []
params = []
if search_query:
where_conditions.append("LOWER(name) LIKE LOWER(?)")
params.append(f"%{search_query}%")
if letter and letter != "all":
if letter == "#":
# Numbers and special characters
where_conditions.append("SUBSTR(UPPER(name), 1, 1) NOT GLOB '[A-Z]'")
else:
# Specific letter
where_conditions.append("UPPER(SUBSTR(name, 1, 1)) = UPPER(?)")
params.append(letter)
# Get active server for filtering
from config.settings import config_manager
active_server = config_manager.get_active_media_server()
# Add active server filter to where conditions
where_conditions.append("a.server_source = ?")
params.append(active_server)
where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
# Get total count (matching dashboard method)
count_query = f"""
SELECT COUNT(*) as total_count
FROM artists a
WHERE {where_clause}
"""
cursor.execute(count_query, params)
total_count = cursor.fetchone()['total_count']
# Get artists with pagination
offset = (page - 1) * limit
artists_query = f"""
SELECT
a.id,
a.name,
a.thumb_url,
a.genres,
COUNT(DISTINCT al.id) as album_count,
COUNT(DISTINCT t.id) as track_count
FROM artists a
LEFT JOIN albums al ON a.id = al.artist_id
LEFT JOIN tracks t ON al.id = t.album_id
WHERE {where_clause}
GROUP BY a.id, a.name, a.thumb_url, a.genres
ORDER BY a.name COLLATE NOCASE
LIMIT ? OFFSET ?
"""
# No need for complex query params now
query_params = params + [limit, offset]
cursor.execute(artists_query, query_params)
rows = cursor.fetchall()
# Convert to artist objects
artists = []
for row in rows:
# Parse genres from GROUP_CONCAT result
genres_str = row['genres'] or ''
genres = []
if genres_str:
# Split by comma and clean up duplicates
genre_set = set()
for genre in genres_str.split(','):
if genre and genre.strip():
genre_set.update(g.strip() for g in genre.split(',') if g.strip())
genres = list(genre_set)
artist = DatabaseArtist(
id=row['id'],
name=row['name'],
thumb_url=row['thumb_url'] if row['thumb_url'] else None,
genres=genres
)
# Add stats
artist_data = {
'id': artist.id,
'name': artist.name,
'image_url': artist.thumb_url,
'genres': artist.genres,
'album_count': row['album_count'] or 0,
'track_count': row['track_count'] or 0
}
artists.append(artist_data)
# Calculate pagination info
total_pages = (total_count + limit - 1) // limit
has_prev = page > 1
has_next = page < total_pages
return {
'artists': artists,
'pagination': {
'page': page,
'limit': limit,
'total_count': total_count,
'total_pages': total_pages,
'has_prev': has_prev,
'has_next': has_next
}
}
except Exception as e:
logger.error(f"Error getting library artists: {e}")
return {
'artists': [],
'pagination': {
'page': 1,
'limit': limit,
'total_count': 0,
'total_pages': 0,
'has_prev': False,
'has_next': False
}
}
def get_artist_discography(self, artist_id) -> Dict[str, Any]:
"""
Get complete artist information and their releases from the database.
This will be combined with Spotify data for the full discography view.
Args:
artist_id: The artist ID from the database (string or int)
Returns:
Dict containing artist info and their owned releases
"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
# Get artist information
cursor.execute("""
SELECT
id, name, thumb_url, genres, server_source
FROM artists
WHERE id = ?
""", (artist_id,))
artist_row = cursor.fetchone()
if not artist_row:
return {
'success': False,
'error': f'Artist with ID {artist_id} not found'
}
# Parse genres
genres_str = artist_row['genres'] or ''
genres = []
if genres_str:
genre_set = set()
for genre in genres_str.split(','):
if genre and genre.strip():
genre_set.add(genre.strip())
genres = list(genre_set)
# Get artist's albums with track counts and completion
# Include albums from ALL artists with the same name (fixes duplicate artist issue)
cursor.execute("""
SELECT
a.id,
a.title,
a.year,
a.track_count,
a.thumb_url,
COUNT(t.id) as owned_tracks
FROM albums a
LEFT JOIN tracks t ON a.id = t.album_id
WHERE a.artist_id IN (
SELECT id FROM artists
WHERE name = (SELECT name FROM artists WHERE id = ?)
AND server_source = (SELECT server_source FROM artists WHERE id = ?)
)
GROUP BY a.id, a.title, a.year, a.track_count, a.thumb_url
ORDER BY a.year DESC, a.title
""", (artist_id, artist_id))
album_rows = cursor.fetchall()
# Process albums and categorize by type
albums = []
eps = []
singles = []
# Get total stats for the artist (including all artists with same name)
cursor.execute("""
SELECT
COUNT(DISTINCT a.id) as album_count,
COUNT(DISTINCT t.id) as track_count
FROM albums a
LEFT JOIN tracks t ON a.id = t.album_id
WHERE a.artist_id IN (
SELECT id FROM artists
WHERE name = (SELECT name FROM artists WHERE id = ?)
AND server_source = (SELECT server_source FROM artists WHERE id = ?)
)
""", (artist_id, artist_id))
stats_row = cursor.fetchone()
album_count = stats_row['album_count'] if stats_row else 0
track_count = stats_row['track_count'] if stats_row else 0
for album_row in album_rows:
# Calculate completion percentage
expected_tracks = album_row['track_count'] or 1
owned_tracks = album_row['owned_tracks'] or 0
completion_percentage = min(100, round((owned_tracks / expected_tracks) * 100))
album_data = {
'id': album_row['id'],
'title': album_row['title'],
'year': album_row['year'],
'image_url': album_row['thumb_url'],
'owned': True, # All albums in our DB are owned
'track_count': album_row['track_count'],
'owned_tracks': owned_tracks,
'track_completion': completion_percentage
}
# Categorize based on actual track count and title patterns
# Use actual owned tracks, fallback to expected track count, then to 0
actual_track_count = owned_tracks or album_row['track_count'] or 0
title_lower = album_row['title'].lower()
# Check for single indicators in title
single_indicators = ['single', ' - single', '(single)']
is_single_by_title = any(indicator in title_lower for indicator in single_indicators)
# Check for EP indicators in title
ep_indicators = ['ep', ' - ep', '(ep)', 'extended play']
is_ep_by_title = any(indicator in title_lower for indicator in ep_indicators)
# Categorization logic - be more conservative about singles
# Only treat as single if explicitly labeled as single AND has few tracks
if is_single_by_title and actual_track_count <= 3:
singles.append(album_data)
elif is_ep_by_title or (4 <= actual_track_count <= 7):
eps.append(album_data)
else:
# Default to album for most releases, especially if track count is unknown
albums.append(album_data)
# Fix image URLs if needed
artist_image_url = artist_row['thumb_url']
if artist_image_url and artist_image_url.startswith('/library/'):
# This will be fixed in the API layer
pass
return {
'success': True,
'artist': {
'id': artist_row['id'],
'name': artist_row['name'],
'image_url': artist_image_url,
'genres': genres,
'server_source': artist_row['server_source'],
'album_count': album_count,
'track_count': track_count
},
'owned_releases': {
'albums': albums,
'eps': eps,
'singles': singles
}
}
except Exception as e:
logger.error(f"Error getting artist discography for ID {artist_id}: {e}")
return {
'success': False,
'error': str(e)
}
# Thread-safe singleton pattern for database access
_database_instances: Dict[int, MusicDatabase] = {} # Thread ID -> Database instance
_database_lock = threading.Lock()