Fixed Spotify stuff
This commit is contained in:
@@ -145,7 +145,7 @@ class DeezerClient:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Last.fm API error: {e}")
|
||||
return ""
|
||||
def import_track(self, track_id, lastfm_api_key=None):
|
||||
def import_track(self, track_id, lastfm_api_key=None):
|
||||
"""
|
||||
Import a track from Deezer into the database
|
||||
Returns a tuple (Song object, was_new) where was_new indicates if this was a new import
|
||||
@@ -279,7 +279,7 @@ class DeezerClient:
|
||||
db.session.rollback()
|
||||
self.logger.error(f"Error saving track to database: {e}")
|
||||
return None, False
|
||||
def import_album(self, album_id, lastfm_api_key=None):
|
||||
def import_album(self, album_id, lastfm_api_key=None):
|
||||
"""
|
||||
Import all tracks from an album
|
||||
Returns a dictionary with import statistics
|
||||
@@ -307,7 +307,7 @@ class DeezerClient:
|
||||
'imported_count': len(imported_songs),
|
||||
'skipped_count': len(skipped_songs)
|
||||
}
|
||||
def import_playlist(self, playlist_id, lastfm_api_key=None):
|
||||
def import_playlist(self, playlist_id, lastfm_api_key=None):
|
||||
"""
|
||||
Import all tracks from a playlist
|
||||
Returns a dictionary with import statistics
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
"""
|
||||
Unified import helper for importing music content across different services.
|
||||
This module provides consistent import functionality for tracks, albums, and playlists
|
||||
from various music streaming services like Spotify and Deezer.
|
||||
from various music streaming services like Spoti try:
|
||||
imported_songs = ImportHelper.import_spotify_playlist(spotify_client, item_id)
|
||||
if not imported_songs or imported_songs.get('imported_count', 0) == 0:
|
||||
current_app.logger.warning(f"Spotify playlist import returned empty result for playlist ID: {item_id}")
|
||||
return {
|
||||
'imported_count': 0,
|
||||
'skipped_count': 0,
|
||||
'error_count': 1,
|
||||
'errors': [f"No songs found in Spotify playlist {item_id} or playlist import failed."]
|
||||
}
|
||||
return imported_songser.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
@@ -240,7 +250,7 @@ class ImportHelper:
|
||||
elif item_type.lower() == 'playlist':
|
||||
try:
|
||||
imported_songs = ImportHelper.import_spotify_playlist(spotify_client, item_id)
|
||||
if not imported_songs or len(imported_songs) == 0:
|
||||
if not imported_songs or imported_songs.get('imported_count', 0) == 0:
|
||||
current_app.logger.warning(f"Spotify playlist import returned empty result for playlist ID: {item_id}")
|
||||
return {
|
||||
'imported_count': 0,
|
||||
@@ -278,8 +288,9 @@ class ImportHelper:
|
||||
'error_count': 1,
|
||||
'errors': ["Deezer client not configured."]
|
||||
}
|
||||
|
||||
lastfm_api_key = current_app.config.get('LASTFM_API_KEY') if item_type.lower() == 'track':
|
||||
lastfm_api_key = current_app.config.get('LASTFM_API_KEY')
|
||||
|
||||
if item_type.lower() == 'track':
|
||||
song, was_new = deezer_client.import_track(item_id, lastfm_api_key=lastfm_api_key)
|
||||
if song:
|
||||
if was_new:
|
||||
@@ -535,9 +546,9 @@ class ImportHelper:
|
||||
# Fetch audio features
|
||||
ImportHelper._fetch_audio_features_for_song(sp, song, track_id, token=authlib_token_for_request) # Pass token
|
||||
|
||||
# Final commit
|
||||
db.session.commit()
|
||||
# Final commit db.session.commit()
|
||||
result['imported_count'] += 1
|
||||
result['song_id'] = song.id # Add the song ID to the result
|
||||
current_app.logger.info(f"Successfully imported Spotify track {track_id} as '{song.title}' with ID {song.id}")
|
||||
else:
|
||||
result['errors'].append(f"Failed to create song object for track {track_id}")
|
||||
@@ -703,7 +714,8 @@ class ImportHelper:
|
||||
'imported_count': 0,
|
||||
'skipped_count': 0,
|
||||
'error_count': 0,
|
||||
'errors': []
|
||||
'errors': [],
|
||||
'imported_song_ids': [] # Track IDs of successfully imported songs
|
||||
}
|
||||
|
||||
if not current_user or not current_user.is_authenticated or not current_user.spotify_token:
|
||||
@@ -783,8 +795,7 @@ class ImportHelper:
|
||||
result['errors'].append(f"Found a track with no ID in playlist {playlist_id}.")
|
||||
result['error_count'] +=1
|
||||
continue
|
||||
|
||||
# Call import_spotify_track. It will handle its own token.
|
||||
# Call import_spotify_track. It will handle its own token.
|
||||
track_import_result = ImportHelper.import_spotify_track(sp, track_id)
|
||||
|
||||
result['imported_count'] += track_import_result.get('imported_count', 0)
|
||||
@@ -792,6 +803,10 @@ class ImportHelper:
|
||||
result['error_count'] += track_import_result.get('error_count', 0)
|
||||
if track_import_result.get('errors'):
|
||||
result['errors'].extend(track_import_result['errors'])
|
||||
|
||||
# Track the song ID if it was successfully imported
|
||||
if track_import_result.get('imported_count', 0) > 0 and track_import_result.get('song_id'):
|
||||
result['imported_song_ids'].append(track_import_result['song_id'])
|
||||
|
||||
tracks_url = tracks_data.get('next')
|
||||
if tracks_url:
|
||||
|
||||
+52
-3
@@ -323,9 +323,58 @@ class SystemSetting(db.Model):
|
||||
db.session.add(setting)
|
||||
else:
|
||||
setting.value = value
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
db.session.commit() @staticmethod
|
||||
def all_settings():
|
||||
return {s.key: s.value for s in SystemSetting.query.all()}
|
||||
|
||||
|
||||
class ImportJobRecord(db.Model):
|
||||
"""
|
||||
Database model for tracking import jobs
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
service_name = db.Column(db.String(50), nullable=False)
|
||||
item_type = db.Column(db.String(20), nullable=False)
|
||||
item_id = db.Column(db.String(255), nullable=False)
|
||||
priority = db.Column(db.Integer, nullable=False, default=10)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
status = db.Column(db.String(20), default='pending') # pending, processing, completed, failed
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
started_at = db.Column(db.DateTime)
|
||||
completed_at = db.Column(db.DateTime)
|
||||
error_message = db.Column(db.Text)
|
||||
imported_count = db.Column(db.Integer, default=0)
|
||||
skipped_count = db.Column(db.Integer, default=0)
|
||||
|
||||
# Relationships
|
||||
user = db.relationship('User', backref=db.backref('import_jobs', lazy=True))
|
||||
|
||||
def __repr__(self):
|
||||
return f"ImportJobRecord(id={self.id}, service={self.service_name}, type={self.item_type}, item_id={self.item_id}, status={self.status})"
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""Calculate the job duration in seconds."""
|
||||
if self.started_at and self.completed_at:
|
||||
return (self.completed_at - self.started_at).total_seconds()
|
||||
return None
|
||||
|
||||
@property
|
||||
def item_url(self):
|
||||
"""Generate a URL to the imported item based on service and type."""
|
||||
if self.service_name == 'spotify':
|
||||
if self.item_type == 'playlist':
|
||||
return f"https://open.spotify.com/playlist/{self.item_id}"
|
||||
elif self.item_type == 'album':
|
||||
return f"https://open.spotify.com/album/{self.item_id}"
|
||||
elif self.item_type == 'track':
|
||||
return f"https://open.spotify.com/track/{self.item_id}"
|
||||
elif self.service_name == 'deezer':
|
||||
if self.item_type == 'playlist':
|
||||
return f"https://www.deezer.com/playlist/{self.item_id}"
|
||||
elif self.item_type == 'album':
|
||||
return f"https://www.deezer.com/album/{self.item_id}"
|
||||
elif self.item_type == 'track':
|
||||
return f"https://www.deezer.com/track/{self.item_id}"
|
||||
return None
|
||||
|
||||
|
||||
@@ -353,22 +353,56 @@ def get_songs_from_deezer_playlist(playlist_id):
|
||||
def get_songs_from_spotify_playlist(playlist_id):
|
||||
"""
|
||||
Fetch songs from a Spotify playlist, properly import them with metadata, and return them
|
||||
Always returns the songs in the playlist, even if all already exist in the DB.
|
||||
"""
|
||||
try:
|
||||
songs_per_round = current_app.config.get('SONGS_PER_ROUND', 10)
|
||||
|
||||
imported_songs = ImportHelper.import_item(
|
||||
import_result = ImportHelper.import_item(
|
||||
item_id=playlist_id,
|
||||
item_type='playlist',
|
||||
source='spotify',
|
||||
service_name='spotify',
|
||||
oauth_spotify=oauth.spotify
|
||||
)
|
||||
|
||||
if not imported_songs:
|
||||
current_app.logger.warning(f"No songs returned from ImportHelper.import_item for Spotify playlist {playlist_id}")
|
||||
|
||||
from musicround.models import Song
|
||||
|
||||
# If we have imported_song_ids, use them (these are DB IDs)
|
||||
if import_result.get('imported_song_ids'):
|
||||
song_db_ids = import_result['imported_song_ids']
|
||||
imported_songs = Song.query.filter(Song.id.in_(song_db_ids)).all()
|
||||
return imported_songs[:songs_per_round]
|
||||
|
||||
# If no imported_song_ids, fetch all Spotify IDs from the playlist and get those songs from DB
|
||||
# Use the Spotify API directly to get the playlist track IDs
|
||||
sp = oauth.spotify
|
||||
# Get playlist tracks (paginated)
|
||||
all_spotify_ids = []
|
||||
next_url = f'playlists/{playlist_id}/tracks'
|
||||
authlib_token = {
|
||||
'access_token': current_user.spotify_token,
|
||||
'refresh_token': current_user.spotify_refresh_token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_at': int(current_user.spotify_token_expiry.timestamp()) if current_user.spotify_token_expiry else None
|
||||
}
|
||||
while next_url:
|
||||
resp = sp.get(next_url, token=authlib_token)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
for item in data.get('items', []):
|
||||
track = item.get('track')
|
||||
if track and track.get('id'):
|
||||
all_spotify_ids.append(track['id'])
|
||||
next_url = data.get('next')
|
||||
# If next_url is a full URL, convert to relative for sp.get
|
||||
if next_url and next_url.startswith('https://api.spotify.com/v1/'):
|
||||
next_url = next_url.replace('https://api.spotify.com/v1/', '')
|
||||
if not all_spotify_ids:
|
||||
current_app.logger.warning(f"No valid tracks found in Spotify playlist {playlist_id}")
|
||||
return []
|
||||
|
||||
return imported_songs[:songs_per_round]
|
||||
# Query all songs in DB with those Spotify IDs, preserving playlist order
|
||||
songs_by_spotify_id = {s.spotify_id: s for s in Song.query.filter(Song.spotify_id.in_(all_spotify_ids)).all()}
|
||||
ordered_songs = [songs_by_spotify_id[sid] for sid in all_spotify_ids if sid in songs_by_spotify_id]
|
||||
return ordered_songs[:songs_per_round]
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error fetching or importing Spotify playlist {playlist_id}: {e}")
|
||||
import traceback
|
||||
|
||||
@@ -6,7 +6,7 @@ import time
|
||||
import random
|
||||
from datetime import datetime # Add datetime import
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, current_app, flash, session, jsonify
|
||||
from flask_login import current_user
|
||||
from flask_login import current_user, login_required
|
||||
from musicround.models import Song, db
|
||||
from musicround.routes.import_songs import import_pl
|
||||
from musicround.helpers.import_helper import ImportHelper
|
||||
@@ -140,20 +140,34 @@ def import_official_playlists():
|
||||
elif not auth_token:
|
||||
flash("No active Spotify session. Please connect your Spotify account.", "warning")
|
||||
return redirect(url_for('users.spotify_link'))
|
||||
|
||||
# Handle POST request for importing a playlist
|
||||
# Handle POST request for importing a playlist
|
||||
if request.method == 'POST':
|
||||
playlist_id = request.form['playlist_id']
|
||||
result = ImportHelper.import_item('spotify', 'playlist', playlist_id)
|
||||
|
||||
if result['imported_count'] > 0:
|
||||
flash(f'Successfully imported {result["imported_count"]} songs from official Spotify playlist!', 'success')
|
||||
elif result['skipped_count'] > 0 and result['error_count'] == 0:
|
||||
flash(f'All {result["skipped_count"]} songs were already in the database.', 'info')
|
||||
elif result['error_count'] > 0:
|
||||
flash(f'Encountered {result["error_count"]} errors during import.', 'warning')
|
||||
else:
|
||||
flash(f'Error importing playlist: {", ".join(result["errors"])}', 'danger')
|
||||
# Check if user is authenticated for queue system
|
||||
if not current_user.is_authenticated:
|
||||
flash("Please log in to import playlists.", "warning")
|
||||
return redirect(url_for('users.login'))
|
||||
|
||||
# Get the import queue from app config
|
||||
queue = current_app.config.get('import_queue')
|
||||
if not queue:
|
||||
flash("Import queue not initialized.", "danger")
|
||||
return redirect(url_for('core.view_songs'))
|
||||
|
||||
# Create import job and add to queue
|
||||
from musicround.helpers.import_queue import ImportJob
|
||||
priority = int(request.form.get('priority', 10))
|
||||
|
||||
job = ImportJob(
|
||||
priority=priority,
|
||||
service_name='spotify',
|
||||
item_type='playlist',
|
||||
item_id=playlist_id,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
queue.add_job(job)
|
||||
flash('Official Spotify playlist import queued successfully. You will be notified when it completes.', 'info')
|
||||
|
||||
return redirect(url_for('core.view_songs'))
|
||||
|
||||
@@ -280,22 +294,35 @@ def direct_official_playlists():
|
||||
# Initialize direct Spotify client with bearer token
|
||||
from musicround.helpers.spotify_direct import SpotifyDirectClient
|
||||
direct_client = SpotifyDirectClient(bearer_token=bearer_token)
|
||||
|
||||
# Handle POST request for importing a playlist
|
||||
# Handle POST request for importing a playlist
|
||||
if request.method == 'POST':
|
||||
playlist_id = request.form['playlist_id']
|
||||
# Use the new unified ImportHelper
|
||||
result = ImportHelper.import_item('spotify', 'playlist', playlist_id)
|
||||
|
||||
if result['imported_count'] > 0:
|
||||
flash(f'Successfully imported {result["imported_count"]} songs from official Spotify playlist!', 'success')
|
||||
elif result['skipped_count'] > 0 and result['error_count'] == 0:
|
||||
flash(f'All {result["skipped_count"]} songs were already in the database.', 'info')
|
||||
elif result['error_count'] > 0:
|
||||
flash(f'Encountered {result["error_count"]} errors during import.', 'warning')
|
||||
else:
|
||||
flash(f'Error importing playlist: {", ".join(result["errors"])}', 'danger')
|
||||
|
||||
# Check if user is authenticated for queue system
|
||||
if not current_user.is_authenticated:
|
||||
flash("Please log in to import playlists.", "warning")
|
||||
return redirect(url_for('users.login'))
|
||||
|
||||
# Get the import queue from app config
|
||||
queue = current_app.config.get('import_queue')
|
||||
if not queue:
|
||||
flash("Import queue not initialized.", "danger")
|
||||
return redirect(url_for('core.view_songs'))
|
||||
|
||||
# Create import job and add to queue
|
||||
from musicround.helpers.import_queue import ImportJob
|
||||
priority = int(request.form.get('priority', 10))
|
||||
|
||||
job = ImportJob(
|
||||
priority=priority,
|
||||
service_name='spotify',
|
||||
item_type='playlist',
|
||||
item_id=playlist_id,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
queue.add_job(job)
|
||||
flash('Direct Spotify playlist import queued successfully. You will be notified when it completes.', 'info')
|
||||
|
||||
return redirect(url_for('core.view_songs'))
|
||||
|
||||
# Get filter keywords from the query string (default to empty list)
|
||||
@@ -713,4 +740,95 @@ def update_direct_token():
|
||||
current_app.logger.error(f"Error validating bearer token: {e}")
|
||||
flash(f'Error validating token: {str(e)}', 'error')
|
||||
|
||||
return redirect(return_url)
|
||||
return redirect(return_url)
|
||||
|
||||
|
||||
@import_bp.route('/queue-status')
|
||||
@login_required
|
||||
def queue_status():
|
||||
"""
|
||||
Display real-time status of the import queue for administrators
|
||||
"""
|
||||
# Check if user is an admin
|
||||
if not current_user.is_admin():
|
||||
flash('Admin access required for Import Queue view.', 'danger')
|
||||
return redirect(url_for('core.index'))
|
||||
|
||||
# Helper function to get current time
|
||||
from datetime import datetime
|
||||
def now():
|
||||
return datetime.utcnow()
|
||||
|
||||
# Get the import queue from app config
|
||||
queue = current_app.config.get('import_queue')
|
||||
if not queue:
|
||||
flash("Import queue not initialized.", "danger")
|
||||
return redirect(url_for('core.view_songs'))
|
||||
|
||||
# Access queue internals for display - this won't modify the queue
|
||||
queue_size = queue._queue.qsize()
|
||||
|
||||
# Extract information about jobs in the queue (without removing them)
|
||||
# This is a bit of a hack but necessary to see what's in the PriorityQueue
|
||||
# without removing items
|
||||
queue_snapshot = []
|
||||
if hasattr(queue._queue, 'queue'):
|
||||
# Make a copy of the internal queue list
|
||||
with queue._lock: # Ensure thread safety while accessing the queue
|
||||
queue_items = list(queue._queue.queue)
|
||||
|
||||
for priority, counter, job in queue_items:
|
||||
queue_snapshot.append({
|
||||
'priority': priority,
|
||||
'counter': counter,
|
||||
'service': job.service_name,
|
||||
'type': job.item_type,
|
||||
'item_id': job.item_id,
|
||||
'user_id': job.user_id
|
||||
})
|
||||
|
||||
# Get active and recent jobs from database if available
|
||||
active_jobs = []
|
||||
recent_jobs = []
|
||||
|
||||
# Check if ImportJobRecord is defined
|
||||
try:
|
||||
from musicround.models import ImportJobRecord
|
||||
|
||||
# Get last 50 jobs from the database, sorted by most recent first
|
||||
recent_jobs = ImportJobRecord.query.order_by(ImportJobRecord.created_at.desc()).limit(50).all()
|
||||
|
||||
# Get the active jobs (status='processing')
|
||||
active_jobs = ImportJobRecord.query.filter_by(status='processing').all()
|
||||
except (ImportError, AttributeError):
|
||||
# ImportJobRecord might not be defined yet, handle this case
|
||||
pass
|
||||
|
||||
# Get some basic stats
|
||||
stats = {
|
||||
'queue_size': queue_size,
|
||||
'active_jobs': len(active_jobs),
|
||||
'completed_today': 0,
|
||||
'failed_today': 0
|
||||
}
|
||||
|
||||
# If we have ImportJobRecord, get some stats
|
||||
if recent_jobs:
|
||||
import datetime
|
||||
today = datetime.datetime.utcnow().date()
|
||||
for job in recent_jobs:
|
||||
if job.completed_at and job.completed_at.date() == today:
|
||||
if job.status == 'completed':
|
||||
stats['completed_today'] += 1
|
||||
elif job.status == 'failed':
|
||||
stats['failed_today'] += 1
|
||||
|
||||
return render_template(
|
||||
'import_queue_status.html',
|
||||
stats=stats,
|
||||
active_jobs=active_jobs,
|
||||
recent_jobs=recent_jobs,
|
||||
queue_snapshot=queue_snapshot,
|
||||
queue=queue,
|
||||
now=now
|
||||
)
|
||||
@@ -165,12 +165,16 @@
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-download mr-2"></i> Backup Manager
|
||||
</span>
|
||||
</a></li>
|
||||
<li><a class="block px-4 py-2 hover:bg-navy-50" href="{{ url_for('users.system_health') }}">
|
||||
</a></li> <li><a class="block px-4 py-2 hover:bg-navy-50" href="{{ url_for('users.system_health') }}">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-heartbeat mr-2"></i> System Health
|
||||
</span>
|
||||
</a></li>
|
||||
<li><a class="block px-4 py-2 hover:bg-navy-50" href="{{ url_for('import.queue_status') }}">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-tasks mr-2"></i> Import Queue
|
||||
</span>
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user