fix database issue
This commit is contained in:
parent
00cb285202
commit
cf33e17060
1 changed files with 333 additions and 7 deletions
|
|
@ -196,7 +196,7 @@ class MusicDatabase:
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Create indexes for performance
|
# 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_albums_artist_id ON albums (artist_id)")
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_album_id ON tracks (album_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:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
cursor.execute("SELECT COUNT(*) FROM artists")
|
cursor.execute("SELECT COUNT(DISTINCT name) FROM artists")
|
||||||
artist_count = cursor.fetchone()[0]
|
artist_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
cursor.execute("SELECT COUNT(*) FROM albums")
|
cursor.execute("SELECT COUNT(*) FROM albums")
|
||||||
|
|
@ -412,8 +412,8 @@ class MusicDatabase:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
if server_source:
|
if server_source:
|
||||||
# Get counts for specific server
|
# Get counts for specific server (deduplicate by name like general count)
|
||||||
cursor.execute("SELECT COUNT(*) FROM artists WHERE server_source = ?", (server_source,))
|
cursor.execute("SELECT COUNT(DISTINCT name) FROM artists WHERE server_source = ?", (server_source,))
|
||||||
artist_count = cursor.fetchone()[0]
|
artist_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
cursor.execute("SELECT COUNT(*) FROM albums WHERE server_source = ?", (server_source,))
|
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)
|
# Convert artist ID to string (handles both Plex integer IDs and Jellyfin GUIDs)
|
||||||
artist_id = str(artist_obj.ratingKey)
|
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)
|
thumb_url = getattr(artist_obj, 'thumb', None)
|
||||||
|
|
||||||
# Only preserve timestamps and flags from summary, not full biography
|
# Only preserve timestamps and flags from summary, not full biography
|
||||||
|
|
@ -593,23 +599,44 @@ class MusicDatabase:
|
||||||
if exists:
|
if exists:
|
||||||
# Update existing artist
|
# Update existing artist
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE artists
|
UPDATE artists
|
||||||
SET name = ?, thumb_url = ?, genres = ?, summary = ?, updated_at = CURRENT_TIMESTAMP
|
SET name = ?, thumb_url = ?, genres = ?, summary = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ? AND server_source = ?
|
WHERE id = ? AND server_source = ?
|
||||||
""", (name, thumb_url, genres_json, summary, artist_id, 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:
|
else:
|
||||||
# Insert new artist
|
# Insert new artist
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO artists (id, name, thumb_url, genres, summary, server_source)
|
INSERT INTO artists (id, name, thumb_url, genres, summary, server_source)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""", (artist_id, name, thumb_url, genres_json, summary, server_source))
|
""", (artist_id, name, thumb_url, genres_json, summary, server_source))
|
||||||
|
logger.debug(f"Inserted new {server_source} artist: {name} (ID: {artist_id})")
|
||||||
|
|
||||||
conn.commit()
|
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
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error inserting/updating {server_source} artist {getattr(artist_obj, 'title', 'Unknown')}: {e}")
|
logger.error(f"Error inserting/updating {server_source} artist {getattr(artist_obj, 'title', 'Unknown')}: {e}")
|
||||||
return False
|
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]:
|
def get_artist(self, artist_id: int) -> Optional[DatabaseArtist]:
|
||||||
"""Get artist by ID"""
|
"""Get artist by ID"""
|
||||||
|
|
@ -2219,6 +2246,305 @@ 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]:
|
||||||
|
"""
|
||||||
|
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
|
# Thread-safe singleton pattern for database access
|
||||||
_database_instances: Dict[int, MusicDatabase] = {} # Thread ID -> Database instance
|
_database_instances: Dict[int, MusicDatabase] = {} # Thread ID -> Database instance
|
||||||
_database_lock = threading.Lock()
|
_database_lock = threading.Lock()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue