library page update
This commit is contained in:
parent
cd9acd115d
commit
329e665db9
3 changed files with 130 additions and 15 deletions
|
|
@ -43,6 +43,9 @@ class JellyfinArtist:
|
|||
|
||||
# Create summary property from Jellyfin data (used for timestamp storage)
|
||||
self.summary = jellyfin_data.get('Overview', '') or ''
|
||||
|
||||
# Create thumb property for artist images
|
||||
self.thumb = self._get_artist_image_url()
|
||||
|
||||
def _parse_date(self, date_str: Optional[str]) -> Optional[datetime]:
|
||||
"""Parse Jellyfin date string to datetime"""
|
||||
|
|
@ -53,6 +56,14 @@ class JellyfinArtist:
|
|||
return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
except:
|
||||
return None
|
||||
|
||||
def _get_artist_image_url(self) -> Optional[str]:
|
||||
"""Generate Jellyfin artist image URL"""
|
||||
if not self.ratingKey:
|
||||
return None
|
||||
|
||||
# Jellyfin primary image URL format
|
||||
return f"/Items/{self.ratingKey}/Images/Primary"
|
||||
|
||||
def albums(self) -> List['JellyfinAlbum']:
|
||||
"""Get all albums for this artist"""
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ class NavidromeArtist:
|
|||
# Create summary property (used for timestamp storage)
|
||||
self.summary = navidrome_data.get('biography', '') or ''
|
||||
|
||||
# Create thumb property for artist images
|
||||
self.thumb = self._get_artist_image_url()
|
||||
|
||||
def _parse_date(self, date_str: Optional[str]) -> Optional[datetime]:
|
||||
"""Parse Navidrome date string to datetime"""
|
||||
if not date_str:
|
||||
|
|
@ -56,6 +59,14 @@ class NavidromeArtist:
|
|||
except:
|
||||
return None
|
||||
|
||||
def _get_artist_image_url(self) -> Optional[str]:
|
||||
"""Generate Navidrome artist image URL using Subsonic getCoverArt API"""
|
||||
if not self.ratingKey:
|
||||
return None
|
||||
|
||||
# Subsonic getCoverArt API for artist images
|
||||
return f"/rest/getCoverArt?id={self.ratingKey}"
|
||||
|
||||
def albums(self) -> List['NavidromeAlbum']:
|
||||
"""Get all albums for this artist"""
|
||||
return self._client.get_albums_for_artist(self.ratingKey)
|
||||
|
|
|
|||
123
web_server.py
123
web_server.py
|
|
@ -1535,6 +1535,51 @@ def get_status():
|
|||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/fix-navidrome-urls', methods=['POST'])
|
||||
def fix_navidrome_urls():
|
||||
"""Fix Navidrome artist image URLs to use correct Subsonic format"""
|
||||
try:
|
||||
db = get_database()
|
||||
with db._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all Navidrome artists with old URL format
|
||||
cursor.execute('SELECT id, name, thumb_url FROM artists WHERE server_source = "navidrome" AND thumb_url LIKE "/api/artist/%"')
|
||||
artists = cursor.fetchall()
|
||||
|
||||
if not artists:
|
||||
return jsonify({"status": "success", "message": "No URLs needed fixing", "updated": 0})
|
||||
|
||||
# Update URLs to new Subsonic format
|
||||
import re
|
||||
updated = 0
|
||||
examples = []
|
||||
|
||||
for artist_id, name, old_url in artists:
|
||||
# Extract artist ID from old URL: /api/artist/ARTIST_ID/image
|
||||
match = re.search(r'/api/artist/([^/]+)/image', old_url)
|
||||
if match:
|
||||
artist_spotify_id = match.group(1)
|
||||
new_url = f'/rest/getCoverArt?id={artist_spotify_id}'
|
||||
|
||||
cursor.execute('UPDATE artists SET thumb_url = ? WHERE id = ? AND server_source = "navidrome"', (new_url, artist_id))
|
||||
updated += 1
|
||||
|
||||
if len(examples) < 3: # Show first 3 as examples
|
||||
examples.append(f'{name}: {old_url} -> {new_url}')
|
||||
|
||||
conn.commit()
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": f"Updated {updated} Navidrome artist URLs to Subsonic format",
|
||||
"updated": updated,
|
||||
"examples": examples
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
@app.route('/api/system/stats')
|
||||
def get_system_stats():
|
||||
"""Get system statistics for dashboard"""
|
||||
|
|
@ -2602,7 +2647,8 @@ def fix_artist_image_url(thumb_url):
|
|||
thumb_url.startswith('https://localhost:') or
|
||||
thumb_url.startswith('/library/') or # Plex relative paths
|
||||
thumb_url.startswith('/Items/') or # Jellyfin relative paths
|
||||
thumb_url.startswith('/api/') # Navidrome API paths
|
||||
thumb_url.startswith('/api/') or # Old Navidrome API paths
|
||||
thumb_url.startswith('/rest/') # Navidrome Subsonic API paths
|
||||
)
|
||||
|
||||
if needs_fixing:
|
||||
|
|
@ -2660,12 +2706,14 @@ def fix_artist_image_url(thumb_url):
|
|||
elif active_server == 'navidrome':
|
||||
navidrome_config = config_manager.get_navidrome_config()
|
||||
navidrome_base_url = navidrome_config.get('base_url', '')
|
||||
print(f"🔧 Navidrome config - base_url: {navidrome_base_url}")
|
||||
navidrome_username = navidrome_config.get('username', '')
|
||||
navidrome_password = navidrome_config.get('password', '')
|
||||
print(f"🔧 Navidrome config - base_url: {navidrome_base_url}, username: {navidrome_username}")
|
||||
|
||||
if navidrome_base_url:
|
||||
if navidrome_base_url and navidrome_username and navidrome_password:
|
||||
# Extract the path from URL
|
||||
if thumb_url.startswith('/api/'):
|
||||
# Already a path
|
||||
if thumb_url.startswith('/rest/'):
|
||||
# Already a Subsonic API path
|
||||
path = thumb_url
|
||||
else:
|
||||
# Full localhost URL, extract path
|
||||
|
|
@ -2673,8 +2721,18 @@ def fix_artist_image_url(thumb_url):
|
|||
parsed = urlparse(thumb_url)
|
||||
path = parsed.path
|
||||
|
||||
# Construct proper Navidrome URL
|
||||
fixed_url = f"{navidrome_base_url.rstrip('/')}{path}"
|
||||
# Generate Subsonic API authentication
|
||||
import hashlib
|
||||
import secrets
|
||||
salt = secrets.token_hex(6)
|
||||
token = hashlib.md5((navidrome_password + salt).encode()).hexdigest()
|
||||
|
||||
# Add authentication parameters to the URL
|
||||
separator = '&' if '?' in path else '?'
|
||||
auth_params = f"u={navidrome_username}&t={token}&s={salt}&v=1.16.1&c=SoulSync&f=json"
|
||||
|
||||
# Construct proper Navidrome Subsonic URL
|
||||
fixed_url = f"{navidrome_base_url.rstrip('/')}{path}{separator}{auth_params}"
|
||||
print(f"🔧 Fixed URL: {fixed_url}")
|
||||
return fixed_url
|
||||
|
||||
|
|
@ -12354,17 +12412,19 @@ def start_oauth_callback_servers():
|
|||
# ===============================================
|
||||
|
||||
def get_spotify_artist_discography(artist_name):
|
||||
"""Get complete artist discography from Spotify"""
|
||||
"""Get complete artist discography from Spotify using proper matching"""
|
||||
try:
|
||||
from core.spotify_client import SpotifyClient
|
||||
from core.matching_engine import MusicMatchingEngine
|
||||
|
||||
print(f"🎵 Searching Spotify for artist: {artist_name}")
|
||||
|
||||
# Initialize Spotify client
|
||||
# Initialize clients
|
||||
spotify_client = SpotifyClient()
|
||||
matching_engine = MusicMatchingEngine()
|
||||
|
||||
# Search for the artist
|
||||
artists = spotify_client.search_artists(artist_name, limit=1)
|
||||
# Search for multiple potential matches (not just 1)
|
||||
artists = spotify_client.search_artists(artist_name, limit=5)
|
||||
|
||||
if not artists:
|
||||
return {
|
||||
|
|
@ -12372,10 +12432,43 @@ def get_spotify_artist_discography(artist_name):
|
|||
'error': f'Artist "{artist_name}" not found on Spotify'
|
||||
}
|
||||
|
||||
artist = artists[0]
|
||||
# Since database names are exact Spotify names, try exact match first
|
||||
best_match = None
|
||||
highest_score = 0.0
|
||||
|
||||
# Step 1: Try exact case-insensitive match
|
||||
for spotify_artist in artists:
|
||||
if artist_name.lower().strip() == spotify_artist.name.lower().strip():
|
||||
print(f"🎯 Exact match found: '{spotify_artist.name}'")
|
||||
best_match = spotify_artist
|
||||
highest_score = 1.0
|
||||
break
|
||||
|
||||
# Step 2: If no exact match, use matching engine with higher threshold
|
||||
if not best_match:
|
||||
db_artist_normalized = matching_engine.normalize_string(artist_name)
|
||||
|
||||
for spotify_artist in artists:
|
||||
spotify_artist_normalized = matching_engine.normalize_string(spotify_artist.name)
|
||||
score = matching_engine.similarity_score(db_artist_normalized, spotify_artist_normalized)
|
||||
|
||||
print(f"🔍 Fuzzy match candidate: '{spotify_artist.name}' (score: {score:.3f})")
|
||||
|
||||
if score > highest_score:
|
||||
highest_score = score
|
||||
best_match = spotify_artist
|
||||
|
||||
# Require high confidence threshold since DB should have exact names
|
||||
if not best_match or highest_score < 0.95:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'No confident artist match found for "{artist_name}" (best: "{getattr(best_match, "name", "N/A")}", score: {highest_score:.3f})'
|
||||
}
|
||||
|
||||
artist = best_match
|
||||
spotify_artist_id = artist.id
|
||||
|
||||
print(f"🎵 Found Spotify artist: {artist.name} (ID: {spotify_artist_id})")
|
||||
print(f"🎵 Found Spotify artist: {artist.name} (ID: {spotify_artist_id}, confidence: {highest_score:.3f})")
|
||||
|
||||
# Get all albums (albums, singles, and compilations)
|
||||
all_albums = spotify_client.get_artist_albums(spotify_artist_id, album_type='album,single,compilation', limit=50)
|
||||
|
|
@ -12505,9 +12598,9 @@ def merge_discography_data(owned_releases, spotify_discography):
|
|||
|
||||
# Calculate track completion using Spotify track count
|
||||
spotify_track_count = spotify_release.get('track_count', 0)
|
||||
owned_track_count = owned_release.get('track_count', 0)
|
||||
owned_track_count = owned_release.get('track_count') or 0
|
||||
|
||||
if spotify_track_count > 0:
|
||||
if spotify_track_count > 0 and owned_track_count is not None:
|
||||
completion_percentage = (owned_track_count / spotify_track_count) * 100
|
||||
owned_release['track_completion'] = {
|
||||
'owned_tracks': owned_track_count,
|
||||
|
|
|
|||
Loading…
Reference in a new issue