Initial clean commit

This commit is contained in:
Christian Krakau-Louis
2025-05-13 08:59:02 +00:00
commit 03b982e7c2
113 changed files with 22973 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+179
View File
@@ -0,0 +1,179 @@
"""
Authentication routes for the Music Round application
"""
import os
from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app, session
from flask_login import login_user, current_user, logout_user, login_required
from werkzeug.security import check_password_hash
from musicround.models import User, db
from datetime import datetime
import spotipy
# Create blueprint
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/')
def index():
"""Landing page"""
if current_user.is_authenticated:
return redirect(url_for('core.index'))
return render_template('auth/index.html')
@auth_bp.route('/login')
def login():
"""Redirect to user login page"""
return redirect(url_for('users.login'))
@auth_bp.route('/login-with-spotify')
def login_with_spotify():
"""Start Spotify OAuth flow for login"""
# If user is already logged in, redirect to home
if current_user.is_authenticated:
return redirect(url_for('core.index'))
# Create a new OAuth object
sp_oauth = current_app.config['sp_oauth']
# Get the authorization URL
auth_url = sp_oauth.get_authorize_url()
# Store state in session for validation
session['oauth_state'] = sp_oauth.state
# Set flag that we're using OAuth for login, not just connection
session['spotify_login_flow'] = True
return redirect(auth_url)
@auth_bp.route('/callback')
def callback():
"""Handle Spotify OAuth callback for login"""
try:
# Verify the state parameter
if request.args.get('state') != session.get('oauth_state'):
flash("Authentication state mismatch. Please try logging in again.", "danger")
return redirect(url_for('auth.index'))
# Get the authorization code
code = request.args.get('code')
if not code:
flash("No authorization code received from Spotify.", "danger")
return redirect(url_for('auth.index'))
# Exchange the code for an access token
sp_oauth = current_app.config['sp_oauth']
token_info = sp_oauth.get_access_token(code)
if not token_info or 'access_token' not in token_info:
flash("Failed to obtain access token from Spotify.", "danger")
return redirect(url_for('auth.index'))
# Store the token in the session
session['access_token'] = token_info['access_token']
session['refresh_token'] = token_info.get('refresh_token')
session['token_expiration'] = token_info.get('expires_at')
session['token_source'] = 'user'
# Get user info from Spotify to find or create the user account
sp = spotipy.Spotify(auth=token_info['access_token'])
spotify_user_info = sp.current_user()
if not spotify_user_info or 'id' not in spotify_user_info:
flash("Could not fetch user information from Spotify.", "danger")
return redirect(url_for('auth.index'))
spotify_id = spotify_user_info['id']
email = spotify_user_info.get('email')
display_name = spotify_user_info.get('display_name', spotify_id)
# Log the Spotify login attempt
current_app.logger.info(f"Spotify login attempt: ID={spotify_id}, Email={email}, Name={display_name}")
# Look for an existing user with this Spotify ID
user = User.query.filter_by(oauth_id=spotify_id).first()
# If no user found with this Spotify ID but we have an email, try to find by email
if not user and email:
user = User.query.filter_by(email=email).first()
if user:
# Update the user's Spotify ID if they have an account with the same email
user.oauth_id = spotify_id
current_app.logger.info(f"Linked Spotify ID {spotify_id} to existing account: {user.username}")
# If we still don't have a user, create a new one
if not user:
if not email:
# If Spotify didn't provide an email, we can't create a new user automatically
flash("Your Spotify account does not have an email address. Please register manually.", "danger")
return redirect(url_for('users.register'))
# Generate a unique username based on Spotify display name
base_username = ''.join(c for c in display_name if c.isalnum()).lower()
if not base_username:
base_username = "spotify_user"
username = base_username
count = 1
while User.query.filter_by(username=username).first():
username = f"{base_username}{count}"
count += 1
# Create a new user
from werkzeug.security import generate_password_hash
import secrets
# Generate a random password - user can reset it later
random_password = secrets.token_urlsafe(12)
user = User(
username=username,
email=email,
password_hash=generate_password_hash(random_password),
first_name=display_name.split()[0] if ' ' in display_name else display_name,
last_name=' '.join(display_name.split()[1:]) if ' ' in display_name else '',
oauth_id=spotify_id,
created_at=datetime.now(),
last_login=datetime.now()
)
try:
db.session.add(user)
db.session.commit()
current_app.logger.info(f"Created new user from Spotify: {username} (ID: {user.id})")
flash(f"Welcome! A new account has been created for you as '{username}'.", "success")
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error creating user from Spotify: {e}")
flash("Error creating account. Please try again or register manually.", "danger")
return redirect(url_for('users.register'))
# Store the Spotify tokens in the user's account
user.spotify_token = token_info['access_token']
user.spotify_refresh_token = token_info.get('refresh_token')
if 'expires_at' in token_info:
user.spotify_token_expiry = datetime.fromtimestamp(token_info['expires_at'])
# Update last login time
user.last_login = datetime.now()
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error updating user with Spotify tokens: {e}")
flash("Error updating your account with Spotify information.", "danger")
return redirect(url_for('users.login'))
# Log the user in
login_user(user)
# Update the Spotify client with the new token
current_app.config['sp'].set_auth(token_info['access_token'])
flash("Successfully logged in with Spotify!", "success")
return redirect(url_for('core.index'))
except Exception as e:
current_app.logger.error(f"Error during Spotify callback: {e}")
flash("Error during Spotify authentication. Please try again.", "danger")
return redirect(url_for('auth.index'))
+323
View File
@@ -0,0 +1,323 @@
"""
Core routes that form the basic navigation structure of the app.
"""
from flask import Blueprint, render_template, redirect, url_for, current_app, request, send_from_directory, abort, session
from flask_login import current_user, login_required
from musicround import db
core_bp = Blueprint('core', __name__)
@core_bp.route('/')
def index():
"""
If user is not logged in, show login.
Otherwise, show homepage with user info.
"""
if not current_user.is_authenticated:
return redirect(url_for('users.login'))
return render_template('homepage.html', user_info={'display_name': current_user.username})
@core_bp.route('/search', methods=['GET'])
@login_required
def search():
"""
Show search page for Spotify
"""
return render_template('service_search.html',
service_name='Spotify',
search_results_url=url_for('core.search_results'),
track_import_url=url_for('import_songs.import_song'),
album_import_url=url_for('import_songs.import_album'),
playlist_import_url=url_for('import_songs.import_playlist'),
url_placeholder="https://open.spotify.com/track/...")
@core_bp.route('/search-results', methods=['POST'])
@login_required
def search_results():
"""Process Spotify search and display results"""
if 'access_token' not in session:
# Redirect to Spotify login if not authenticated
return redirect(url_for('auth.spotify_login'))
search_term = request.form.get('search_term', '')
if not search_term:
return redirect(url_for('core.search'))
try:
# Initialize Spotify client with access token
import spotipy
from spotipy.exceptions import SpotifyException
current_app.logger.info(f"Searching Spotify for: {search_term}")
# Try to check if token is valid before using it
try:
sp = spotipy.Spotify(auth=session.get('access_token'))
# Make a simple API call to verify token
sp.current_user()
except SpotifyException as e:
# If token is expired, try refreshing it
if e.http_status == 401:
current_app.logger.info("Spotify token expired, attempting refresh")
# Check if we have a refresh token
if current_user.spotify_refresh_token:
try:
# Create OAuth object to refresh token
from spotipy.oauth2 import SpotifyOAuth
from musicround.config import Config
sp_oauth = SpotifyOAuth(
client_id=Config.SPOTIFY_CLIENT_ID,
client_secret=Config.SPOTIFY_CLIENT_SECRET,
redirect_uri=Config.SPOTIFY_REDIRECT_URI,
scope=Config.SPOTIFY_SCOPE
)
# Get new token
token_info = sp_oauth.refresh_access_token(current_user.spotify_refresh_token)
# Update session and user
session['access_token'] = token_info['access_token']
current_user.spotify_token = token_info['access_token']
if 'refresh_token' in token_info:
current_user.spotify_refresh_token = token_info['refresh_token']
# Update token expiry
import datetime
current_user.spotify_token_expiry = datetime.datetime.now() + datetime.timedelta(seconds=token_info['expires_in'])
# Save changes
db.session.commit()
# Create new Spotify client with updated token
sp = spotipy.Spotify(auth=token_info['access_token'])
except Exception as refresh_error:
current_app.logger.error(f"Error refreshing Spotify token: {str(refresh_error)}")
# Redirect to login if we can't refresh
return redirect(url_for('auth.spotify_login'))
else:
# No refresh token, redirect to login
return redirect(url_for('auth.spotify_login'))
else:
# Some other Spotify error
raise
# Prepare more specific search parameters for better results
# Try different search strategies for artists vs tracks
search_strategies = [
# Regular search for all types
{'q': search_term, 'type': 'track,album,playlist', 'limit': 10},
# Search specifically for artist
{'q': f'artist:{search_term}', 'type': 'track', 'limit': 10},
# Search specifically for track
{'q': f'track:{search_term}', 'type': 'track', 'limit': 10}
]
tracks = []
albums = []
playlists = []
# Try different search strategies until we get results
for strategy in search_strategies:
current_app.logger.info(f"Trying search strategy: {strategy}")
# Perform search with current strategy
results = sp.search(**strategy)
# Extract track results if available
if 'tracks' in results and results['tracks']['items']:
for item in results['tracks']['items']:
# Skip None items or items without required fields
if item is None or 'id' not in item or 'artists' not in item or 'album' not in item:
continue
artist_names = [artist['name'] for artist in item['artists']]
# Get album image safely
image_url = None
if 'album' in item and item['album'] and 'images' in item['album'] and item['album']['images']:
image_url = item['album']['images'][0]['url'] if item['album']['images'] else None
# Get album name safely
album_name = item['album']['name'] if 'album' in item and item['album'] and 'name' in item['album'] else 'Unknown Album'
tracks.append({
'id': item['id'],
'name': item['name'],
'artist': ', '.join(artist_names),
'album': album_name,
'image_url': image_url,
'preview_url': item.get('preview_url'),
'duration_ms': item.get('duration_ms', 0)
})
# Extract album results if available
if 'albums' in results and results['albums']['items']:
for item in results['albums']['items']:
# Skip None items or items without required fields
if item is None or 'id' not in item or 'artists' not in item:
continue
artist_names = [artist['name'] for artist in item['artists']]
# Get album image safely
image_url = None
if 'images' in item and item['images']:
image_url = item['images'][0]['url'] if item['images'] else None
albums.append({
'id': item['id'],
'name': item['name'],
'artist': ', '.join(artist_names),
'image_url': image_url,
'total_tracks': item.get('total_tracks', 0)
})
# Extract playlist results if available
if 'playlists' in results and results['playlists']['items']:
for item in results['playlists']['items']:
# Skip None items or items without required fields
if item is None or 'id' not in item or 'owner' not in item:
continue
# Get playlist image safely
image_url = None
if 'images' in item and item['images']:
image_url = item['images'][0]['url'] if item['images'] else None
# Get track count safely
track_count = 0
if 'tracks' in item and item['tracks'] is not None and 'total' in item['tracks']:
track_count = item['tracks']['total']
# Get owner name safely
owner_name = item['owner'].get('display_name') or item['owner'].get('id', 'Unknown')
playlists.append({
'id': item['id'],
'name': item['name'],
'owner': owner_name,
'image_url': image_url,
'tracks': track_count
})
# If we got any results, break the loop
if tracks or albums or playlists:
break
# If still no results after all strategies, try one more approach
if not tracks and not albums and not playlists:
current_app.logger.info("No results from standard searches, trying market-specific search")
# Try a more generic search with market specification
results = sp.search(q=search_term, type='track,album,playlist', limit=10, market='US')
# Extract track results
if 'tracks' in results and results['tracks']['items']:
for item in results['tracks']['items']:
artist_names = [artist['name'] for artist in item['artists']]
tracks.append({
'id': item['id'],
'name': item['name'],
'artist': ', '.join(artist_names),
'album': item['album']['name'],
'image_url': item['album']['images'][0]['url'] if item['album']['images'] else None,
'preview_url': item['preview_url'],
'duration_ms': item['duration_ms']
})
# Remove duplicates (in case our strategies found the same items)
unique_tracks = []
track_ids_seen = set()
for track in tracks:
if track['id'] not in track_ids_seen:
track_ids_seen.add(track['id'])
unique_tracks.append(track)
unique_albums = []
album_ids_seen = set()
for album in albums:
if album['id'] not in album_ids_seen:
album_ids_seen.add(album['id'])
unique_albums.append(album)
unique_playlists = []
playlist_ids_seen = set()
for playlist in playlists:
if playlist['id'] not in playlist_ids_seen:
playlist_ids_seen.add(playlist['id'])
unique_playlists.append(playlist)
# Log the number of results found
current_app.logger.info(f"Search results: {len(unique_tracks)} tracks, {len(unique_albums)} albums, {len(unique_playlists)} playlists")
# Render search results template
return render_template('service_search_results.html',
service_name='Spotify',
search_term=search_term,
tracks=unique_tracks,
albums=unique_albums,
playlists=unique_playlists,
track_import_url=url_for('import_songs.import_song'),
album_import_url=url_for('import_songs.import_album'),
playlist_import_url=url_for('import_songs.import_playlist'),
track_id_field='song_id',
album_id_field='album_id',
playlist_id_field='playlist_id',
tracks_label='Tracks',
has_preview=True,
search_url=url_for('core.search'))
except Exception as e:
# Log the detailed error
import traceback
current_app.logger.error(f"Spotify search error: {str(e)}")
current_app.logger.error(traceback.format_exc())
# Render error template
return render_template('error.html',
error_message="An error occurred while searching Spotify.",
error_details=str(e),
back_url=url_for('core.search'))
@core_bp.route('/view-songs')
@login_required
def view_songs():
"""
Show all songs in database
"""
from musicround.models import Song, Tag
# Get all songs
songs = Song.query.all()
# Get all tags
tags = Tag.query.all()
return render_template('view_songs.html', songs=songs, tags=tags)
@core_bp.route('/data/<path:filepath>')
@login_required
def serve_user_audio(filepath):
"""
Serve user custom audio files from the data directory
"""
# For security, ensure the filepath doesn't try to access parent directories
if '..' in filepath:
abort(404)
# Only allow access to the current user's custom MP3 files or to admins
if 'custommp3/' in filepath:
# Extract username from the filepath
parts = filepath.split('/')
if len(parts) >= 2 and parts[0] == 'custommp3':
username = parts[1]
# Check if current user is the owner of the file or an admin
if username != current_user.username and not current_user.is_admin:
abort(403) # Unauthorized
return send_from_directory('/data', filepath)
+190
View File
@@ -0,0 +1,190 @@
from flask import Blueprint, redirect, url_for, flash, jsonify
from flask_admin import Admin, BaseView, expose
from flask_admin.contrib.sqla import ModelView
from flask_admin.contrib.fileadmin import FileAdmin
from flask_admin.menu import MenuLink
from flask_admin.actions import action
from flask_login import current_user, login_required
from musicround.models import Song, Tag, SongTag, Round, User, Role, UserPreferences, SystemSetting, db
from functools import wraps
import os
import json
# Create a basic authentication wrapper
def admin_required(view_func):
@wraps(view_func)
def wrapper(*args, **kwargs):
# Check if user is logged in and is an admin
if not current_user.is_authenticated:
return redirect(url_for('users.login'))
if not current_user.is_admin():
flash('Admin access required.', 'danger')
return redirect(url_for('core.index'))
return view_func(*args, **kwargs)
return wrapper
# Create the blueprint
db_admin_bp = Blueprint('db_admin', __name__, url_prefix='/admin')
# Define routes on the blueprint before it gets registered
@db_admin_bp.route('/raw')
@admin_required
def raw_db_access():
return redirect(url_for('admin.index'))
# Base model view with authentication
class AuthModelView(ModelView):
def is_accessible(self):
return current_user.is_authenticated and current_user.is_admin()
def inaccessible_callback(self, name, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for('users.login'))
return redirect(url_for('core.index'))
# Add basic search functionality to all models
column_searchable_list = []
column_filters = []
# Enable export to CSV
can_export = True
export_types = ['csv', 'json']
# Enhanced Song ModelView
class SongModelView(AuthModelView):
column_searchable_list = ['title', 'artist', 'spotify_id']
column_filters = ['title', 'artist', 'year', 'genre', 'used_count']
column_default_sort = ('id', False)
@action('reset_used_count', 'Reset Used Count', 'Are you sure you want to reset used count to 0?')
def action_reset_used_count(self, ids):
try:
query = Song.query.filter(Song.id.in_(ids))
# Update all songs
for song in query.all():
song.used_count = 0
db.session.commit()
flash(f'Used count reset for {len(ids)} songs.', 'success')
except Exception as ex:
db.session.rollback()
flash(f'Error resetting used count: {str(ex)}', 'danger')
# Enhanced Round ModelView
class RoundModelView(AuthModelView):
column_searchable_list = ['name', 'round_type', 'round_criteria_used']
column_filters = ['name', 'round_type', 'round_criteria_used']
column_list = ['id', 'name', 'round_type', 'round_criteria_used', 'created_at', 'mp3_generated', 'pdf_generated']
column_default_sort = ('id', False)
# Enhanced Tag ModelView
class TagModelView(AuthModelView):
column_searchable_list = ['name']
column_filters = ['name']
# Enhanced SongTag ModelView
class SongTagModelView(AuthModelView):
column_filters = ['song_id', 'tag_id']
# Enhanced User ModelView
class UserModelView(AuthModelView):
column_searchable_list = ['username', 'email', 'first_name', 'last_name']
column_filters = ['username', 'email', 'active', 'created_at', 'last_login']
column_default_sort = ('id', False)
# Protect password field in forms
form_excluded_columns = ['password_hash', 'reset_token', 'reset_token_expiry']
@action('activate_users', 'Activate Users', 'Are you sure you want to activate selected users?')
def action_activate_users(self, ids):
try:
query = User.query.filter(User.id.in_(ids))
# Update all selected users
for user in query.all():
user.active = True
db.session.commit()
flash(f'Successfully activated {len(ids)} users.', 'success')
except Exception as ex:
db.session.rollback()
flash(f'Error activating users: {str(ex)}', 'danger')
@action('deactivate_users', 'Deactivate Users', 'Are you sure you want to deactivate selected users?')
def action_deactivate_users(self, ids):
try:
query = User.query.filter(User.id.in_(ids))
# Update all selected users
for user in query.all():
user.active = False
db.session.commit()
flash(f'Successfully deactivated {len(ids)} users.', 'success')
except Exception as ex:
db.session.rollback()
flash(f'Error deactivating users: {str(ex)}', 'danger')
# Enhanced Role ModelView
class RoleModelView(AuthModelView):
column_searchable_list = ['name', 'description']
column_filters = ['name']
# Enhanced UserPreferences ModelView
class UserPreferencesModelView(AuthModelView):
column_filters = ['user_id', 'theme', 'enable_intro']
# Enhanced SystemSetting ModelView
class SystemSettingModelView(AuthModelView):
column_searchable_list = ['key']
column_filters = ['key']
column_exclude_list = [] # Ensure any sensitive values are not excluded if needed
# Initialize the admin interface
admin = None
def init_admin(app):
"""Initialize the admin interface with the Flask app."""
global admin
# Set Flask-Admin configuration
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean' # Use a Bootstrap swatch theme
# Create admin interface
admin = Admin(
app,
name='MusicRound Admin',
template_mode='bootstrap3',
url='/admin'
)
# Add model views
# Data models
admin.add_view(SongModelView(Song, db.session, category="Music Data"))
admin.add_view(TagModelView(Tag, db.session, category="Music Data"))
admin.add_view(SongTagModelView(SongTag, db.session, category="Music Data"))
admin.add_view(RoundModelView(Round, db.session, category="Music Data"))
# User management
admin.add_view(UserModelView(User, db.session, category="User Management"))
admin.add_view(RoleModelView(Role, db.session, category="User Management"))
admin.add_view(UserPreferencesModelView(UserPreferences, db.session, category="User Management"))
# System
admin.add_view(SystemSettingModelView(SystemSetting, db.session, category="System"))
# Add file admin for audio files
path = os.path.join(os.path.dirname(__file__), '../static/audio')
admin.add_view(FileAdmin(path, '/static/audio/', name='Audio Files', category="System", endpoint='static_audio_files'))
# User MP3 files
user_mp3_path = os.path.join(os.path.dirname(__file__), '../mp3')
admin.add_view(FileAdmin(user_mp3_path, '/mp3/', name='User MP3 Files', category="System", endpoint='user_mp3_files'))
# Add links
admin.add_link(MenuLink(name='Back to App', url='/'))
return admin
+223
View File
@@ -0,0 +1,223 @@
from flask import Blueprint, render_template, redirect, url_for, request, current_app, flash, session, jsonify
from musicround.models import Song, db
import deezer
import musicbrainzngs
import requests
import openai
import os
import json
from musicround.helpers.metadata import get_song_metadata_by_isrc
from musicround.helpers.import_helper import ImportHelper
deezer_bp = Blueprint('deezer', __name__)
@deezer_bp.route('/deezer-search', methods=['GET'])
def deezer_search():
"""Display Deezer search form"""
return render_template('service_search.html',
service_name='Deezer',
search_results_url=url_for('deezer.deezer_search_results'),
browse_playlists_url=url_for('deezer.browse_deezer_playlists'),
track_import_url=url_for('deezer.import_deezer_track_result'),
album_import_url=url_for('deezer.import_deezer_album_result'),
playlist_import_url=url_for('deezer.import_deezer_playlist_result'),
url_placeholder='https://www.deezer.com/...')
@deezer_bp.route('/deezer-search-results', methods=['POST'])
def deezer_search_results():
"""Search for tracks, albums, and playlists on Deezer"""
search_term = request.form['search_term']
deezer_client = current_app.config['deezer']
try:
tracks = deezer_client.search_tracks(search_term)
albums = deezer_client.search_albums(search_term)
playlists = deezer_client.search_playlists(search_term)
# Format tracks for the template
formatted_tracks = []
for track in tracks:
if track:
formatted_tracks.append({
'id': track.get('id'),
'name': track.get('title'),
'artist': track.get('artist', {}).get('name', 'Unknown Artist') if track.get('artist') else 'Unknown Artist',
'album': track.get('album', {}).get('title', '') if track.get('album') else '',
'image_url': track.get('album', {}).get('cover_medium') if track.get('album') else None,
'preview_url': track.get('preview')
})
# Format albums for the template
formatted_albums = []
for album in albums:
if album:
formatted_albums.append({
'id': album.get('id'),
'name': album.get('title'),
'artist': album.get('artist', {}).get('name', 'Unknown Artist') if album.get('artist') else 'Unknown Artist',
'image_url': album.get('cover_medium'),
'track_count': album.get('nb_tracks')
})
# Format playlists for the template
formatted_playlists = []
for playlist in playlists:
if playlist:
formatted_playlists.append({
'id': playlist.get('id'),
'name': playlist.get('title'),
'owner': playlist.get('user', {}).get('name', 'Unknown') if playlist.get('user') else 'Unknown',
'image_url': playlist.get('picture_medium'),
'track_count': playlist.get('nb_tracks')
})
# Use the standardized template for search results
return render_template('service_search_results.html',
service_name='Deezer',
search_term=search_term,
tracks=formatted_tracks,
albums=formatted_albums,
playlists=formatted_playlists,
tracks_label='Tracks',
search_url=url_for('deezer.deezer_search'),
has_preview=True,
track_import_url=url_for('deezer.import_deezer_track_result'),
track_id_field='track_id',
album_import_url=url_for('deezer.import_deezer_album_result'),
album_id_field='album_id',
playlist_import_url=url_for('deezer.import_deezer_playlist_result'),
playlist_id_field='playlist_id')
except Exception as e:
current_app.logger.error(f"Deezer search error: {e}")
flash("Error searching Deezer. Please try again.", "danger")
return redirect(url_for('deezer.deezer_search'))
@deezer_bp.route('/import-deezer-track', methods=['GET'])
def import_deezer_track():
"""Display form to import a single track from Deezer"""
return render_template('service_import.html',
service_name='Deezer',
item_type='Track',
url_example_prefix='https://www.deezer.com/track/',
url_example_id='12345678',
id_field='track_id',
form_action=url_for('deezer.import_deezer_track_result'),
back_url=url_for('deezer.deezer_search'))
@deezer_bp.route('/import-deezer-track-result', methods=['POST'])
def import_deezer_track_result():
"""Process the import of a single track from Deezer using the unified ImportHelper"""
track_id = request.form['track_id']
# Use the unified ImportHelper to handle track import
result = ImportHelper.import_item('deezer', 'track', track_id)
if result['imported_count'] > 0:
flash(f'Successfully imported {result["imported_count"]} song from Deezer!', 'success')
elif result['skipped_count'] > 0:
flash('Song was already in the database.', 'info')
else:
flash(f'Error importing song: {", ".join(result["errors"])}', 'danger')
return redirect(url_for('core.view_songs'))
# Legacy function kept for backward compatibility
def import_deezer_track_result_helper(track_id):
"""Helper function to import a single track from Deezer"""
result = ImportHelper.import_item('deezer', 'track', track_id)
return result['imported_count'] > 0
@deezer_bp.route('/import-deezer-playlist', methods=['GET', 'POST'])
def import_deezer_playlist():
"""Display form to import all tracks from a Deezer playlist"""
if request.method == 'POST':
playlist_id = request.form.get('playlist_id')
if playlist_id:
# Use the unified ImportHelper to handle playlist import
result = ImportHelper.import_item('deezer', 'playlist', playlist_id)
if result['imported_count'] > 0:
flash(f'Successfully imported {result["imported_count"]} songs from Deezer 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')
return redirect(url_for('core.view_songs'))
else:
flash("Playlist ID is required.", 'error')
return render_template('service_import.html',
service_name='Deezer',
item_type='Playlist',
url_example_prefix='https://www.deezer.com/playlist/',
url_example_id='9876543',
id_field='playlist_id',
form_action=url_for('deezer.import_deezer_playlist_result'),
back_url=url_for('deezer.deezer_search'))
@deezer_bp.route('/import-deezer-playlist-result', methods=['POST'])
def import_deezer_playlist_result():
"""Process the import of all tracks from a Deezer playlist using the unified ImportHelper"""
playlist_id = request.form['playlist_id']
# Use the unified ImportHelper to handle playlist import
result = ImportHelper.import_item('deezer', 'playlist', playlist_id)
if result['imported_count'] > 0:
flash(f'Successfully imported {result["imported_count"]} songs from Deezer 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')
return redirect(url_for('core.view_songs'))
@deezer_bp.route('/import-deezer-album', methods=['GET'])
def import_deezer_album():
"""Display form to import all tracks from a Deezer album"""
return render_template('service_import.html',
service_name='Deezer',
item_type='Album',
url_example_prefix='https://www.deezer.com/album/',
url_example_id='1234567',
id_field='album_id',
form_action=url_for('deezer.import_deezer_album_result'),
back_url=url_for('deezer.deezer_search'))
@deezer_bp.route('/import-deezer-album-result', methods=['POST'])
def import_deezer_album_result():
"""Process the import of all tracks from a Deezer album using the unified ImportHelper"""
album_id = request.form['album_id']
# Use the unified ImportHelper to handle album import
result = ImportHelper.import_item('deezer', 'album', album_id)
if result['imported_count'] > 0:
flash(f'Successfully imported {result["imported_count"]} songs from Deezer album!', '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 album: {", ".join(result["errors"])}', 'danger')
return redirect(url_for('core.view_songs'))
@deezer_bp.route('/browse-deezer-playlists')
def browse_deezer_playlists():
"""Browse popular playlists on Deezer"""
deezer_client = current_app.config['deezer']
try:
playlists = deezer_client.get_popular_playlists()
return render_template('browse_deezer_playlists.html', playlists=playlists)
except Exception as e:
current_app.logger.error(f"Error browsing Deezer playlists: {e}")
flash("Error loading Deezer playlists. Please try again.", "danger")
return render_template('browse_deezer_playlists.html', playlists=[])
+579
View File
@@ -0,0 +1,579 @@
import random
from datetime import datetime
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
from flask_login import current_user, login_required
from musicround.models import Song, Round, Tag, db
generate_bp = Blueprint('generate', __name__)
# Constants
songs_per_round = 8
# Helper functions
def get_all_decades():
"""
Return a list of 'decade' strings (e.g. '1970', '1980')
based on the first 3 digits of the year + '0'.
"""
all_decades = []
for song in Song.query.all():
if song.year:
decade = str(song.year)[:3] + '0'
if decade not in all_decades:
all_decades.append(decade)
return all_decades
def get_all_genres():
"""
Return a list of all genres in the Song table.
"""
all_genres = []
for song in Song.query.all():
if song.genre and song.genre not in all_genres:
all_genres.append(song.genre)
return all_genres
def get_all_tags():
"""
Return a list of all tag names in the Tag table.
"""
return [tag.name for tag in Tag.query.all()]
def get_songs_by_tag(tag_name, limit=8):
"""
Return songs that have the specified tag.
"""
tag = Tag.query.filter_by(name=tag_name).first()
if tag:
return tag.songs[:limit]
return []
def get_least_used_genres():
"""
Returns a list of genre(s) whose usage count is minimal among all genres.
Usage is measured by how many Rounds of type 'genre' reference that genre.
"""
all_genres_list = get_all_genres()
# Start every genre with usage=0
genre_usage = {g: 0 for g in all_genres_list}
# Count how many times each genre appears in Rounds of type 'genre'
used_genre_rounds = Round.query.filter_by(round_type='genre').all()
for rnd in used_genre_rounds:
# Ensure we only increment if it exists in genre_usage
if rnd.round_criteria_used in genre_usage:
genre_usage[rnd.round_criteria_used] += 1
# If we have no genres at all, return an empty list
if not genre_usage:
return []
# Find the minimal usage count
min_usage = min(genre_usage.values())
# Return all genres that match min_usage
return [g for g, usage in genre_usage.items() if usage == min_usage]
def get_least_used_decades():
"""
Returns a list of decade(s) whose usage count is minimal among all decades.
Usage is measured by how many Rounds of type 'decade' reference that decade.
"""
all_decades_list = get_all_decades()
# Start each decade with usage=0
decade_usage = {d: 0 for d in all_decades_list}
# Count how many times each decade appears in Rounds of type 'decade'
used_decade_rounds = Round.query.filter_by(round_type='decade').all()
for rnd in used_decade_rounds:
if rnd.round_criteria_used in decade_usage:
decade_usage[rnd.round_criteria_used] += 1
# If we have no decades at all, return empty
if not decade_usage:
return []
# Minimal usage
min_usage = min(decade_usage.values())
# Return all decades that match min_usage
return [d for d, usage in decade_usage.items() if usage == min_usage]
def get_least_used_songs(genre=None, decade=None):
"""
Returns songs that have never been used in a round.
Can filter by genre or decade.
"""
least_used_songs = []
all_songs = Song.query.all()
# gather round_criteria_used for rounds of type 'song'
used_song_ids = []
for rnd in Round.query.all():
if rnd.round_type == 'song':
used_song_ids.append(rnd.round_criteria_used)
# If a song's spotify_id never appears in used_song_ids => "least used"
for song in all_songs:
if song.spotify_id not in used_song_ids:
least_used_songs.append(song)
# Filter by genre or decade if passed
if genre:
least_used_songs = [s for s in least_used_songs if s.genre == genre]
if decade:
least_used_songs = [s for s in least_used_songs if s.year and str(s.year)[:3] + '0' == decade]
return least_used_songs
def get_non_overused_songs(genre=None, decade=None):
"""
Returns a list of songs whose used_count is <= the average usage among all songs.
Optional filtering by genre or decade.
"""
all_songs = Song.query.all()
total_times_used = sum(song.used_count for song in all_songs) or 1
average_times_used = total_times_used / len(all_songs) if len(all_songs) else 1
# pick songs that are used <= average usage
non_overused_songs = [s for s in all_songs if s.used_count <= average_times_used]
if genre:
non_overused_songs = [s for s in non_overused_songs if s.genre == genre]
if decade:
non_overused_songs = [s for s in non_overused_songs if s.year and str(s.year)[:3] + '0' == decade]
return non_overused_songs
def get_random_songs_from_genre(genre, x=5):
"""
Returns x random songs from the given genre,
filling from non-overused songs in that genre.
If not enough, fallback to any non-overused songs.
"""
non_overused = get_non_overused_songs(genre=genre)
while len(non_overused) < x:
more = get_non_overused_songs()
if not more: # in case the DB is empty or something else
break
non_overused.extend(more)
return random.sample(non_overused, x) if len(non_overused) >= x else non_overused
def get_random_songs_from_decade(decade, x=5):
"""
Returns x random songs from the given decade,
filling from non-overused songs in that decade.
If not enough, fallback to any non-overused songs.
"""
non_overused = get_non_overused_songs(decade=decade)
while len(non_overused) < x:
more = get_non_overused_songs()
if not more:
break
non_overused.extend(more)
return random.sample(non_overused, x) if len(non_overused) >= x else non_overused
def get_random_songs(x):
"""
Returns x random songs from the pool of non-overused songs,
ensuring some naive diversity constraints:
- no artist used more than once
- no decade used more than x/3 times
- number of unique artists must match number of chosen songs
"""
non_overused_songs = get_non_overused_songs()
if len(non_overused_songs) < x:
return non_overused_songs
random_songs = random.sample(non_overused_songs, x)
artist_count = {}
decade_count = {}
for song in random_songs:
artist_count[song.artist] = artist_count.get(song.artist, 0) + 1
if song.year:
dec = str(song.year)[:3] + '0'
decade_count[dec] = decade_count.get(dec, 0) + 1
while (
max(artist_count.values()) > 1
or (decade_count and max(decade_count.values()) > len(random_songs) / 3)
or len(artist_count) != len(random_songs)
):
# 1. If any artist is used more than once, replace that song
if max(artist_count.values()) > 1:
repeated_artist = None
for artist, count in artist_count.items():
if count > 1:
repeated_artist = artist
break
if repeated_artist:
# remove one of that artist from random_songs
to_remove = next(s for s in random_songs if s.artist == repeated_artist)
random_songs.remove(to_remove)
artist_count[repeated_artist] -= 1
# pick a new random non-overused
refill = [s for s in non_overused_songs if s not in random_songs]
if refill:
new_song = random.choice(refill)
random_songs.append(new_song)
artist_count[new_song.artist] = artist_count.get(new_song.artist, 0) + 1
# update decade_count
if to_remove.year:
dec_to_remove = str(to_remove.year)[:3] + '0'
decade_count[dec_to_remove] = decade_count.get(dec_to_remove, 0) - 1
if new_song.year:
dec_new = str(new_song.year)[:3] + '0'
decade_count[dec_new] = decade_count.get(dec_new, 0) + 1
else:
# If no new songs available, just return what we have
return random_songs
elif decade_count and max(decade_count.values()) > len(random_songs) / 3:
# 2. If any decade is used more than x/3, remove a song from that decade
repeated_decade = None
for dec, count in decade_count.items():
if count > len(random_songs) / 3:
repeated_decade = dec
break
if repeated_decade:
to_remove = next((s for s in random_songs if s.year and str(s.year)[:3] + '0' == repeated_decade), None)
if to_remove:
random_songs.remove(to_remove)
decade_count[repeated_decade] -= 1
# pick a new random
refill = [s for s in non_overused_songs if s not in random_songs]
if refill:
new_song = random.choice(refill)
random_songs.append(new_song)
if new_song.year:
dec_new = str(new_song.year)[:3] + '0'
decade_count[dec_new] = decade_count.get(dec_new, 0) + 1
# update artist_count
artist_count[to_remove.artist] -= 1
artist_count[new_song.artist] = artist_count.get(new_song.artist, 0) + 1
else:
# If no new songs available, just return what we have
return random_songs
elif len(artist_count) != len(random_songs):
# 3. if there's mismatch in how many unique artists vs. songs, fix that
# i.e. if we have a repeated artist but haven't caught it above
refill = [s for s in non_overused_songs if s not in random_songs]
repeated_song = None
# find a repeated artist
for s in random_songs:
if artist_count[s.artist] > 1:
repeated_song = s
break
if repeated_song is None or not refill:
break # fallback
random_songs.remove(repeated_song)
artist_count[repeated_song.artist] -= 1
new_song = random.choice(refill)
random_songs.append(new_song)
artist_count[new_song.artist] = artist_count.get(new_song.artist, 0) + 1
if repeated_song.year:
dec_removed = str(repeated_song.year)[:3] + '0'
decade_count[dec_removed] = decade_count.get(dec_removed, 0) - 1
if new_song.year:
dec_new = str(new_song.year)[:3] + '0'
decade_count[dec_new] = decade_count.get(dec_new, 0) + 1
return random_songs
def get_random_songs_from_least_used_decade(x):
"""
Returns up to x songs from *one* of the least used decade(s), chosen at random.
Returns (songs, chosen_decade).
"""
candidates = get_least_used_decades()
if not candidates:
return [], None
chosen_decade = random.choice(candidates)
random_songs = get_random_songs_from_decade(chosen_decade, x=x)
return random_songs, chosen_decade
def get_random_songs_from_least_used_genre(x):
"""
Returns up to x songs from *one* of the least used genre(s), chosen at random.
Returns (songs, chosen_genre).
"""
candidates = get_least_used_genres()
if not candidates:
return [], None
chosen_genre = random.choice(candidates)
random_songs = get_random_songs_from_genre(chosen_genre, x=x)
return random_songs, chosen_genre
def get_songs_from_deezer_playlist(playlist_id):
"""
Fetch songs from a Deezer playlist, properly import them with metadata, and return them
"""
try:
deezer_client = current_app.config['deezer']
songs_per_round = current_app.config.get('SONGS_PER_ROUND', 10)
playlist = deezer_client.get_playlist(playlist_id)
if not playlist:
return []
# Use the ImportHelper static methods directly without creating an instance
from musicround.helpers.import_helper import ImportHelper
songs = []
tracks = playlist.get('tracks', {}).get('data', [])
for track in tracks:
deezer_id = str(track.get('id'))
if not deezer_id:
continue
# Check if song already exists in our database
existing_song = Song.query.filter_by(deezer_id=deezer_id).first()
if existing_song:
songs.append(existing_song)
continue
# Use the proper ImportHelper static method for importing
track_result = ImportHelper.import_deezer_track(deezer_client, deezer_id)
# If the track was successfully imported, retrieve it from the database
if track_result.get('imported_count', 0) > 0:
imported_song = Song.query.filter_by(deezer_id=deezer_id).first()
if imported_song:
songs.append(imported_song)
return songs[:songs_per_round] # Limit to songs_per_round
except Exception as e:
current_app.logger.error(f"Error fetching Deezer playlist: {e}")
import traceback
current_app.logger.error(traceback.format_exc())
return []
def get_songs_from_spotify_playlist(playlist_id):
"""
Fetch songs from a Spotify playlist, properly import them with metadata, and return them
"""
try:
sp = current_app.config['sp']
songs_per_round = current_app.config.get('SONGS_PER_ROUND', 10)
playlist = sp.playlist_tracks(playlist_id)
if not playlist:
return []
# Use the ImportHelper static methods directly without creating an instance
from musicround.helpers.import_helper import ImportHelper
songs = []
for item in playlist.get('items', []):
track = item.get('track')
if not track or not track.get('id'):
continue
spotify_id = track.get('id')
# Check if the song already exists in our database
existing_song = Song.query.filter_by(spotify_id=spotify_id).first()
if existing_song:
songs.append(existing_song)
continue
# Use the proper ImportHelper static methods for importing
track_result = ImportHelper.import_spotify_track(sp, spotify_id)
# If the track was successfully imported, retrieve it from the database
if track_result.get('imported_count', 0) > 0:
imported_song = Song.query.filter_by(spotify_id=spotify_id).first()
if imported_song:
songs.append(imported_song)
return songs[:songs_per_round] # Limit to songs_per_round
except Exception as e:
current_app.logger.error(f"Error fetching Spotify playlist: {e}")
import traceback
current_app.logger.error(traceback.format_exc())
return []
@generate_bp.route('/build-music-round', methods=['GET', 'POST'])
@login_required
def build_music_round():
"""Build a music round based on the selected criteria"""
if request.method == 'POST':
round_type = request.form['round_type']
if round_type == 'Random':
round_criteria = 'Random'
songs = get_random_songs(songs_per_round)
return render_template('round.html', songs=songs, round_criteria=round_criteria)
elif round_type == 'Decade':
round_criteria = 'Least Used Decade'
songs, decade_used = get_random_songs_from_least_used_decade(songs_per_round)
return render_template(
'round.html',
songs=songs,
round_criteria=round_criteria,
decade=decade_used
)
elif round_type == 'Genre':
round_criteria = 'Least Used Genre'
songs, genre_used = get_random_songs_from_least_used_genre(songs_per_round)
return render_template(
'round.html',
songs=songs,
round_criteria=round_criteria,
genre=genre_used
)
elif round_type == 'Tag':
tag_name = request.form.get('tag_name')
if tag_name:
round_criteria = f'Tag: {tag_name}'
songs = get_songs_by_tag(tag_name, songs_per_round)
return render_template(
'round.html',
songs=songs,
round_criteria=round_criteria,
tag=tag_name
)
# Pass tag choices to the template for selection
tags = get_all_tags()
return render_template('build_music_round.html', tags=tags)
@generate_bp.route('/import-playlist', methods=['GET', 'POST'])
@login_required
def import_playlist():
"""Import a playlist from Deezer or Spotify"""
if request.method == 'POST':
playlist_url = request.form.get('playlist_url', '')
platform = request.form.get('platform', '').lower()
round_name = request.form.get('round_name', '')
if not playlist_url:
flash('Please enter a playlist URL or ID', 'error')
return redirect(url_for('generate.import_playlist'))
# Extract playlist ID from URL or use as is
playlist_id = playlist_url
if platform == 'deezer':
# Extract Deezer playlist ID from URL if needed
if 'deezer.com' in playlist_url:
try:
playlist_id = playlist_url.split('playlist/')[1].split('?')[0]
except (IndexError, ValueError):
flash('Invalid Deezer playlist URL', 'error')
return redirect(url_for('generate.import_playlist'))
songs = get_songs_from_deezer_playlist(playlist_id)
if not songs:
flash('No songs found or error fetching playlist from Deezer', 'error')
return redirect(url_for('generate.import_playlist'))
round_criteria = f'Deezer Playlist: {playlist_id}'
elif platform == 'spotify':
# Extract Spotify playlist ID from URL if needed
if 'spotify.com' in playlist_url:
try:
playlist_id = playlist_url.split('playlist/')[1].split('?')[0]
except (IndexError, ValueError):
flash('Invalid Spotify playlist URL', 'error')
return redirect(url_for('generate.import_playlist'))
songs = get_songs_from_spotify_playlist(playlist_id)
if not songs:
flash('No songs found or error fetching playlist from Spotify', 'error')
return redirect(url_for('generate.import_playlist'))
round_criteria = f'Spotify Playlist: {playlist_id}'
else:
flash('Please select a valid platform', 'error')
return redirect(url_for('generate.import_playlist'))
return render_template('round.html',
songs=songs,
round_criteria=round_criteria,
round_name=round_name,
playlist_import=True)
return render_template('import_playlist.html')
@generate_bp.route('/save_round', methods=['POST'])
@login_required
def save_round():
"""
Persists a new Round to the DB (with chosen songs).
Increments used_count on all chosen songs.
"""
# get round criteria and name from form
round_criteria = request.form.get('round_criteria')
round_name = request.form.get('round_name')
# get optional genre and decade from form
genre = request.form.get('genre')
decade = request.form.get('decade')
tag = request.form.get('tag')
# get list of song IDs from form
song_ids = request.form.getlist('song_id')
# get list of song objects from database
songs = Song.query.filter(Song.id.in_(song_ids)).all()
# create string representation of song IDs
song_ids_str = ','.join(song_id for song_id in song_ids)
# determine round type
if genre:
round_type = 'Genre'
round_criteria_used = genre
elif decade:
round_type = 'Decade'
round_criteria_used = decade
elif tag:
round_type = 'Tag'
round_criteria_used = tag
else:
round_type = 'Random'
round_criteria_used = 'Random Selection'
# create new Round object and add to database
new_round = Round(
name=round_name,
round_type=round_type,
round_criteria_used=round_criteria_used,
songs=song_ids_str,
created_at=datetime.utcnow()
)
db.session.add(new_round)
# update usage count for each song
for song in songs:
song.used_count += 1
db.session.add(song)
db.session.commit()
# redirect back to the rounds page
return redirect(url_for('rounds.rounds_list'))
+241
View File
@@ -0,0 +1,241 @@
"""
Import routes for the Music Round application
"""
import json
import time
import random
from flask import Blueprint, render_template, redirect, url_for, request, current_app, flash, session, jsonify
from musicround.models import Song, db
from musicround.routes.import_songs import import_pl, import_track
import_bp = Blueprint('import', __name__, url_prefix='/import')
def fetch_all_user_playlists(sp, user_id, limit=50):
"""
Fetch all playlists from a specific Spotify user account with pagination
Args:
sp: Spotify API client
user_id: Spotify user ID to fetch playlists from
limit: Number of playlists to fetch per request (max 50)
Returns:
List of all playlists from the specified user
"""
all_playlists = []
offset = 0
total = None
start_time = time.time()
current_app.logger.info(f"Started fetching playlists for user '{user_id}'")
while total is None or offset < total:
try:
# Use Spotify API to get playlists with pagination
results = sp.user_playlists(user_id, limit=limit, offset=offset)
# If first request, get the total
if total is None:
total = results['total']
current_app.logger.info(f"User '{user_id}' has {total} playlists in total")
# Add the current batch of playlists to our collection
playlists_batch = results.get('items', [])
all_playlists.extend(playlists_batch)
# Update offset for next batch
offset += limit
# Log progress
current_app.logger.info(f"Fetched {len(playlists_batch)} playlists for '{user_id}', progress: {len(all_playlists)}/{total}")
# Break if we've reached the end
if not results.get('next'):
break
except Exception as e:
current_app.logger.error(f"Error fetching playlists for user '{user_id}' at offset {offset}: {e}")
break
end_time = time.time()
duration = int((end_time - start_time) * 1000)
current_app.logger.info(f"Completed fetching {len(all_playlists)}/{total} playlists for user '{user_id}' in {duration}ms")
return all_playlists
def filter_playlists_by_keywords(playlists, keywords, debug_info=None):
"""
Filter playlists by checking if any of the keywords are in the playlist name
Args:
playlists: List of playlist objects
keywords: List of keywords to filter by
debug_info: Optional debug info dictionary to update with filtering stats
Returns:
Filtered list of playlists
"""
filtered = []
keywords_lower = [k.lower() for k in keywords]
for playlist in playlists:
name = playlist.get('name', '').lower()
# Check if any keyword is in the playlist name
if any(keyword in name for keyword in keywords_lower):
filtered.append(playlist)
# Add to debug info if provided
if debug_info is not None and 'matched_keywords' in debug_info:
matched = [k for k in keywords_lower if k in name]
for keyword in matched:
if keyword not in debug_info['matched_keywords']:
debug_info['matched_keywords'][keyword] = 0
debug_info['matched_keywords'][keyword] += 1
return filtered
@import_bp.route('/official-playlists', methods=['GET', 'POST'])
def import_official_playlists():
"""Display and import official Spotify playlists from multiple regional accounts"""
if 'access_token' not in session:
return redirect(url_for('auth.login'))
sp = current_app.config['sp']
# Handle POST request for importing a playlist
if request.method == 'POST':
playlist_id = request.form['playlist_id']
import_pl(playlist_id)
flash('Spotify playlist imported successfully!', 'success')
return redirect(url_for('core.view_songs'))
# Get filter keywords from the query string (default to empty list)
filter_keywords = request.args.get('filter', '').split(',')
filter_keywords = [k.strip() for k in filter_keywords if k.strip()]
# List of official Spotify user accounts to fetch playlists from
spotify_accounts = [
'spotify',
'spotifycharts',
'spotifymaps',
'spotifyuk',
'spotifyusa',
'spotify_germany'
]
# Get selected account from query string or default to all
selected_account = request.args.get('account', 'all')
# Get debug mode parameter
debug_mode = request.args.get('debug', 'false').lower() == 'true'
# Prepare debug info
debug_info = {
'accounts': {},
'total_fetched': 0,
'total_filtered': 0,
'filtered_out': 0,
'matched_keywords': {},
'query_time_ms': 0,
'duplicates_removed': 0
}
# Initialize playlists list
all_playlists = []
try:
start_time = time.time()
# Process each Spotify account or just the selected one
accounts_to_process = [selected_account] if selected_account != 'all' else spotify_accounts
for account in accounts_to_process:
if account not in spotify_accounts and account != 'all':
continue
account_debug = {
'total': 0,
'fetched': 0,
'filtered': 0,
'time_ms': 0
}
account_start = time.time()
# Fetch all playlists for this account
account_playlists = fetch_all_user_playlists(sp, account)
account_end = time.time()
account_debug['time_ms'] = int((account_end - account_start) * 1000)
account_debug['total'] = len(account_playlists)
account_debug['fetched'] = len(account_playlists)
debug_info['total_fetched'] += len(account_playlists)
# Apply keyword filtering if keywords provided
if filter_keywords:
filtered_playlists = filter_playlists_by_keywords(
account_playlists,
filter_keywords,
debug_info
)
account_debug['filtered'] = len(filtered_playlists)
debug_info['filtered_out'] += (len(account_playlists) - len(filtered_playlists))
all_playlists.extend(filtered_playlists)
else:
# No filtering, use all playlists
all_playlists.extend(account_playlists)
account_debug['filtered'] = len(account_playlists)
debug_info['accounts'][account] = account_debug
# Remove duplicates based on playlist ID
unique_playlists = []
seen_ids = set()
for playlist in all_playlists:
if playlist['id'] not in seen_ids:
seen_ids.add(playlist['id'])
unique_playlists.append(playlist)
else:
debug_info['duplicates_removed'] += 1
all_playlists = unique_playlists
debug_info['total_filtered'] = len(all_playlists)
end_time = time.time()
debug_info['query_time_ms'] = int((end_time - start_time) * 1000)
# Log summary
current_app.logger.info(
f"Search summary: Fetched {debug_info['total_fetched']} playlists, "
f"filtered to {debug_info['total_filtered']} playlists "
f"({debug_info['filtered_out']} filtered out, {debug_info['duplicates_removed']} duplicates removed) "
f"in {debug_info['query_time_ms']}ms"
)
# Sort playlists by follower count or name if available
all_playlists.sort(key=lambda x: x.get('name', '').lower())
# Randomize order if no specific sorting
if not filter_keywords:
random.shuffle(all_playlists)
except Exception as e:
current_app.logger.error(f"Error fetching official playlists: {e}")
flash('Error retrieving playlists from Spotify', 'danger')
all_playlists = []
# Handle empty result
if not all_playlists:
flash('No Spotify playlists found matching your criteria', 'warning')
return render_template(
'import_official_playlists.html',
playlists=all_playlists,
filter_keywords=filter_keywords,
selected_account=selected_account,
spotify_accounts=spotify_accounts,
debug_info=debug_info,
debug_mode=debug_mode
)
+745
View File
@@ -0,0 +1,745 @@
"""
Import routes for the Music Round application
"""
import json
import time
import random
from flask import Blueprint, render_template, redirect, url_for, request, current_app, flash, session, jsonify
from musicround.models import Song, db
from musicround.routes.import_songs import import_pl
from musicround.helpers.import_helper import ImportHelper
import_bp = Blueprint('import', __name__, url_prefix='/import')
def fetch_all_user_playlists(sp, user_id, limit=50):
"""
Fetch all playlists from a specific Spotify user account with pagination
Args:
sp: Spotify API client
user_id: Spotify user ID to fetch playlists from
limit: Number of playlists to fetch per request (max 50)
Returns:
List of all playlists from the specified user
"""
all_playlists = []
offset = 0
total = None
start_time = time.time()
current_app.logger.info(f"Started fetching playlists for user '{user_id}'")
# Hard limit to prevent infinite loops (should never be needed if API works correctly)
max_loops = 100
loop_count = 0
while loop_count < max_loops:
loop_count += 1
try:
# Use Spotify API to get playlists with pagination
current_app.logger.info(f"Fetching playlists for {user_id} with offset={offset}, limit={limit}, loop={loop_count}")
results = sp.user_playlists(user_id, limit=limit, offset=offset)
# Log raw API response for debugging (only first few characters to avoid flooding logs)
response_sample = str(results)[:500] + '...' if len(str(results)) > 500 else str(results)
current_app.logger.debug(f"API response sample: {response_sample}")
# If first request, get and validate the total
if total is None:
total = results.get('total', 0)
current_app.logger.info(f"User '{user_id}' has {total} playlists in total according to API")
if total == 0:
current_app.logger.warning(f"API reported 0 total playlists for {user_id} - possible API error")
# Add the current batch of playlists to our collection
playlists_batch = results.get('items', [])
batch_count = len(playlists_batch)
all_playlists.extend(playlists_batch)
current_app.logger.info(f"Batch for {user_id}: offset={offset}, received={batch_count} playlists")
# If we didn't get any playlists in this batch, something is wrong
if batch_count == 0:
current_app.logger.warning(f"Received 0 playlists for {user_id} at offset {offset} - possible API error")
if 'items' not in results:
current_app.logger.warning(f"Missing 'items' key in API response for {user_id}")
break
# Break if we received fewer items than requested (last page)
if batch_count < limit:
current_app.logger.info(f"Reached end of results for {user_id} (received {batch_count} < limit {limit})")
break
# Update offset for next batch
offset += batch_count
# Log progress
current_app.logger.info(f"Fetched {len(playlists_batch)} playlists for '{user_id}', progress: {len(all_playlists)}/{total}")
# Break if we've reached or exceeded the total number of playlists
if offset >= total:
current_app.logger.info(f"Reached total {total} playlists for {user_id} at offset {offset}")
break
# Break if we've exhausted all playlists (next URL is None)
if not results.get('next'):
current_app.logger.info(f"No more 'next' URL for {user_id} at offset {offset}")
# Check if we should have more results based on 'total'
if offset < total:
current_app.logger.warning(
f"API inconsistency: 'next' is None but we've only fetched {offset} out of {total} playlists"
)
break
except Exception as e:
current_app.logger.error(f"Error fetching playlists for user '{user_id}' at offset {offset}: {str(e)}")
# Try to get more specific error information
import traceback
current_app.logger.error(f"Traceback: {traceback.format_exc()}")
break
# Check if we hit the max loops limit
if loop_count >= max_loops:
current_app.logger.warning(f"Reached maximum loop count ({max_loops}) for user {user_id}")
end_time = time.time()
duration = int((end_time - start_time) * 1000)
current_app.logger.info(f"Completed fetching {len(all_playlists)}/{total} playlists for user '{user_id}' in {duration}ms")
return all_playlists
def filter_playlists_by_keywords(playlists, keywords, debug_info=None):
"""
Filter playlists by checking if any of the keywords are in the playlist name
Args:
playlists: List of playlist objects
keywords: List of keywords to filter by
debug_info: Optional debug info dictionary to update with filtering stats
Returns:
Filtered list of playlists
"""
filtered = []
keywords_lower = [k.lower() for k in keywords]
for playlist in playlists:
name = playlist.get('name', '').lower()
# Check if any keyword is in the playlist name
if any(keyword in name for keyword in keywords_lower):
filtered.append(playlist)
# Add to debug info if provided
if debug_info is not None and 'matched_keywords' in debug_info:
matched = [k for k in keywords_lower if k in name]
for keyword in matched:
if keyword not in debug_info['matched_keywords']:
debug_info['matched_keywords'][keyword] = 0
debug_info['matched_keywords'][keyword] += 1
return filtered
@import_bp.route('/official-playlists', methods=['GET', 'POST'])
def import_official_playlists():
"""Display and import official Spotify playlists from multiple regional accounts"""
if 'access_token' not in session:
return redirect(url_for('users.login'))
sp = current_app.config['sp']
# 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')
return redirect(url_for('core.view_songs'))
# Get filter keywords from the query string (default to empty list)
filter_keywords = request.args.get('filter', '').split(',')
filter_keywords = [k.strip() for k in filter_keywords if k.strip()]
# List of official Spotify user accounts to fetch playlists from
spotify_accounts = [
'spotify',
'spotifycharts',
'spotifymaps',
'spotifyuk',
'spotifyusa',
'spotify_germany'
]
# Get selected account from query string or default to all
selected_account = request.args.get('account', 'all')
# Get debug mode parameter
debug_mode = request.args.get('debug', 'false').lower() == 'true'
# Prepare debug info
debug_info = {
'accounts': {},
'total_fetched': 0,
'total_filtered': 0,
'filtered_out': 0,
'matched_keywords': {},
'query_time_ms': 0,
'duplicates_removed': 0
}
# Initialize playlists list
all_playlists = []
try:
start_time = time.time()
# Process each Spotify account or just the selected one
accounts_to_process = [selected_account] if selected_account != 'all' else spotify_accounts
for account in accounts_to_process:
if account not in spotify_accounts and account != 'all':
continue
account_debug = {
'total': 0,
'fetched': 0,
'filtered': 0,
'time_ms': 0
}
account_start = time.time()
# Fetch all playlists for this account
account_playlists = fetch_all_user_playlists(sp, account)
account_end = time.time()
account_debug['time_ms'] = int((account_end - account_start) * 1000)
account_debug['total'] = len(account_playlists)
account_debug['fetched'] = len(account_playlists)
debug_info['total_fetched'] += len(account_playlists)
# Apply keyword filtering if keywords provided
if filter_keywords:
filtered_playlists = filter_playlists_by_keywords(
account_playlists,
filter_keywords,
debug_info
)
account_debug['filtered'] = len(filtered_playlists)
debug_info['filtered_out'] += (len(account_playlists) - len(filtered_playlists))
all_playlists.extend(filtered_playlists)
else:
# No filtering, use all playlists
all_playlists.extend(account_playlists)
account_debug['filtered'] = len(account_playlists)
debug_info['accounts'][account] = account_debug
# Remove duplicates based on playlist ID
unique_playlists = []
seen_ids = set()
for playlist in all_playlists:
if playlist['id'] not in seen_ids:
seen_ids.add(playlist['id'])
unique_playlists.append(playlist)
else:
debug_info['duplicates_removed'] += 1
all_playlists = unique_playlists
debug_info['total_filtered'] = len(all_playlists)
end_time = time.time()
debug_info['query_time_ms'] = int((end_time - start_time) * 1000)
# Log summary
current_app.logger.info(
f"Search summary: Fetched {debug_info['total_fetched']} playlists, "
f"filtered to {debug_info['total_filtered']} playlists "
f"({debug_info['filtered_out']} filtered out, {debug_info['duplicates_removed']} duplicates removed) "
f"in {debug_info['query_time_ms']}ms"
)
# Sort playlists by follower count or name if available
all_playlists.sort(key=lambda x: x.get('name', '').lower())
# Randomize order if no specific sorting
if not filter_keywords:
random.shuffle(all_playlists)
except Exception as e:
current_app.logger.error(f"Error fetching official playlists: {e}")
flash('Error retrieving playlists from Spotify', 'danger')
all_playlists = []
# Handle empty result
if not all_playlists:
flash('No Spotify playlists found matching your criteria', 'warning')
# Get the bearer token from the session to display in the form
session_bearer_token = session.get('direct_bearer_token', '')
spotify_username = session.get('direct_spotify_username')
return render_template(
'import_official_playlists.html',
playlists=all_playlists,
filter_keywords=filter_keywords,
selected_account=selected_account,
spotify_accounts=spotify_accounts,
debug_info=debug_info,
debug_mode=debug_mode,
session_bearer_token=session_bearer_token,
spotify_username=spotify_username,
direct_mode=False
)
@import_bp.route('/direct-official-playlists', methods=['GET', 'POST'])
def direct_official_playlists():
"""Display and import official Spotify playlists using the direct client with bearer token"""
# Check if user has provided a bearer token
bearer_token = session.get('direct_bearer_token')
if not bearer_token:
flash('Please provide a bearer token first', 'warning')
return redirect(url_for('import.direct_spotify_auth'))
# 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
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')
return redirect(url_for('core.view_songs'))
# Get filter keywords from the query string (default to empty list)
filter_keywords = request.args.get('filter', '').split(',')
filter_keywords = [k.strip() for k in filter_keywords if k.strip()]
# List of official Spotify user accounts to fetch playlists from
spotify_accounts = [
'spotify',
'spotifycharts',
'spotifymaps',
'spotifyuk',
'spotifyusa',
'spotify_germany'
]
# Get selected account from query string or default to all
selected_account = request.args.get('account', 'all')
# Get debug mode parameter
debug_mode = request.args.get('debug', 'false').lower() == 'true'
# Prepare debug info
debug_info = {
'accounts': {},
'total_fetched': 0,
'total_filtered': 0,
'filtered_out': 0,
'matched_keywords': {},
'query_time_ms': 0,
'duplicates_removed': 0
}
# Initialize playlists list
all_playlists = []
try:
start_time = time.time()
# Process each Spotify account or just the selected one
accounts_to_process = [selected_account] if selected_account != 'all' else spotify_accounts
for account in accounts_to_process:
if account not in spotify_accounts and account != 'all':
continue
account_debug = {
'total': 0,
'fetched': 0,
'filtered': 0,
'time_ms': 0
}
account_start = time.time()
# Fetch all playlists for this account using direct client
account_playlists = direct_client.fetch_all_user_playlists(account)
account_end = time.time()
account_debug['time_ms'] = int((account_end - account_start) * 1000)
account_debug['total'] = len(account_playlists)
account_debug['fetched'] = len(account_playlists)
debug_info['total_fetched'] += len(account_playlists)
# Apply keyword filtering if keywords provided
if filter_keywords:
filtered_playlists = filter_playlists_by_keywords(
account_playlists,
filter_keywords,
debug_info
)
account_debug['filtered'] = len(filtered_playlists)
debug_info['filtered_out'] += (len(account_playlists) - len(filtered_playlists))
all_playlists.extend(filtered_playlists)
else:
# No filtering, use all playlists
all_playlists.extend(account_playlists)
account_debug['filtered'] = len(account_playlists)
debug_info['accounts'][account] = account_debug
# Remove duplicates based on playlist ID
unique_playlists = []
seen_ids = set()
for playlist in all_playlists:
if playlist['id'] not in seen_ids:
seen_ids.add(playlist['id'])
unique_playlists.append(playlist)
else:
debug_info['duplicates_removed'] += 1
all_playlists = unique_playlists
debug_info['total_filtered'] = len(all_playlists)
end_time = time.time()
debug_info['query_time_ms'] = int((end_time - start_time) * 1000)
# Log summary
current_app.logger.info(
f"Direct search summary: Fetched {debug_info['total_fetched']} playlists, "
f"filtered to {debug_info['total_filtered']} playlists "
f"({debug_info['filtered_out']} filtered out, {debug_info['duplicates_removed']} duplicates removed) "
f"in {debug_info['query_time_ms']}ms"
)
# Sort playlists by follower count or name if available
all_playlists.sort(key=lambda x: x.get('name', '').lower())
# Randomize order if no specific sorting
if not filter_keywords:
random.shuffle(all_playlists)
except Exception as e:
current_app.logger.error(f"Error fetching official playlists with direct client: {e}")
import traceback
current_app.logger.error(traceback.format_exc())
flash(f'Error retrieving playlists from Spotify: {str(e)}', 'danger')
all_playlists = []
# Handle empty result
if not all_playlists:
flash('No Spotify playlists found matching your criteria', 'warning')
return render_template(
'import_official_playlists.html',
playlists=all_playlists,
filter_keywords=filter_keywords,
selected_account=selected_account,
spotify_accounts=spotify_accounts,
debug_info=debug_info,
debug_mode=debug_mode,
direct_mode=True,
spotify_username=session.get('direct_spotify_username')
)
@import_bp.route('/test-spotify-client', methods=['GET'])
def test_spotify_client():
"""Test route to compare different Spotify client implementations"""
if 'access_token' not in session:
return redirect(url_for('users.login'))
# Get Spotify account to check from query parameters
account = request.args.get('account', 'spotify')
# Results container
results = {
'spotipy': {
'playlists': [],
'count': 0,
'total': 0,
'time_ms': 0,
'error': None
},
'direct': {
'playlists': [],
'count': 0,
'total': 0,
'time_ms': 0,
'error': None
}
}
# Test spotipy implementation
try:
sp = current_app.config['sp']
start_time = time.time()
current_app.logger.info(f"Testing spotipy implementation for account {account}")
spotipy_playlists = fetch_all_user_playlists(sp, account)
end_time = time.time()
duration_ms = int((end_time - start_time) * 1000)
results['spotipy']['playlists'] = spotipy_playlists
results['spotipy']['count'] = len(spotipy_playlists)
results['spotipy']['time_ms'] = duration_ms
# Get total from first API call if available
if spotipy_playlists:
first_result = sp.user_playlists(account, limit=1)
results['spotipy']['total'] = first_result.get('total', 'unknown')
except Exception as e:
import traceback
current_app.logger.error(f"Error testing spotipy: {e}")
current_app.logger.error(traceback.format_exc())
results['spotipy']['error'] = str(e)
# Test direct implementation
try:
from musicround.helpers.spotify_direct import SpotifyDirectClient
# Get bearer token from session if available
bearer_token = session.get('direct_bearer_token')
if not bearer_token:
current_app.logger.warning("No bearer token in session for direct client")
results['direct']['error'] = "No bearer token available. Please set a token in Direct Auth page first."
else:
direct_client = SpotifyDirectClient(bearer_token=bearer_token)
start_time = time.time()
current_app.logger.info(f"Testing direct implementation for account {account}")
direct_playlists = direct_client.fetch_all_user_playlists(account)
end_time = time.time()
duration_ms = int((end_time - start_time) * 1000)
results['direct']['playlists'] = direct_playlists
results['direct']['count'] = len(direct_playlists)
results['direct']['time_ms'] = duration_ms
# Get total if available from response
if direct_playlists and len(direct_playlists) > 0:
first_result = direct_client.user_playlists(account, limit=1)
results['direct']['total'] = first_result.get('total', 'unknown')
except Exception as e:
import traceback
current_app.logger.error(f"Error testing direct client: {e}")
current_app.logger.error(traceback.format_exc())
results['direct']['error'] = str(e)
# Compare playlists between implementations
comparison = {
'only_in_spotipy': [],
'only_in_direct': [],
'in_both': []
}
if results['spotipy']['playlists'] and results['direct']['playlists']:
spotipy_ids = {pl['id'] for pl in results['spotipy']['playlists']}
direct_ids = {pl['id'] for pl in results['direct']['playlists']}
comparison['only_in_spotipy'] = list(spotipy_ids - direct_ids)
comparison['only_in_direct'] = list(direct_ids - spotipy_ids)
comparison['in_both'] = list(spotipy_ids.intersection(direct_ids))
# Add direct auth link to template data
direct_auth_url = url_for('import.direct_spotify_auth')
# Render the comparison template
return render_template(
'spotify_client_test.html',
account=account,
results=results,
comparison=comparison,
direct_auth_url=direct_auth_url,
has_bearer_token=bool(session.get('direct_bearer_token'))
)
@import_bp.route('/raw-playlists', methods=['GET'])
def get_raw_playlists():
"""
Get raw playlists from Spotify without any pagination logic.
This helps diagnose issues with the playlist retrieval.
"""
if 'access_token' not in session:
return redirect(url_for('users.login'))
# Get Spotify account to check
account = request.args.get('account', 'spotify')
# Get limit parameter (max 50)
limit = min(int(request.args.get('limit', '50')), 50)
# Get offset parameter
offset = int(request.args.get('offset', '0'))
results = {
'spotipy': {
'raw_response': None,
'error': None
},
'direct': {
'raw_response': None,
'error': None
}
}
# Test spotipy raw response
try:
sp = current_app.config['sp']
current_app.logger.info(f"Getting raw playlists with spotipy for {account}, limit={limit}, offset={offset}")
raw_result = sp.user_playlists(account, limit=limit, offset=offset)
results['spotipy']['raw_response'] = raw_result
except Exception as e:
import traceback
current_app.logger.error(f"Error getting raw spotipy playlists: {e}")
current_app.logger.error(traceback.format_exc())
results['spotipy']['error'] = str(e)
# Test direct API raw response
try:
from musicround.helpers.spotify_direct import SpotifyDirectClient
# Get bearer token from session if available
bearer_token = session.get('direct_bearer_token')
if not bearer_token:
current_app.logger.warning("No bearer token in session for direct client")
results['direct']['error'] = "No bearer token available. Please set a token in Direct Auth page first."
else:
direct_client = SpotifyDirectClient(bearer_token=bearer_token)
current_app.logger.info(f"Getting raw playlists with direct API for {account}, limit={limit}, offset={offset}")
raw_result = direct_client.user_playlists(account, limit=limit, offset=offset)
results['direct']['raw_response'] = raw_result
except Exception as e:
import traceback
current_app.logger.error(f"Error getting raw direct playlists: {e}")
current_app.logger.error(traceback.format_exc())
results['direct']['error'] = str(e)
# Add direct auth link to template data
direct_auth_url = url_for('import.direct_spotify_auth')
return render_template(
'raw_playlists.html',
account=account,
limit=limit,
offset=offset,
results=results,
direct_auth_url=direct_auth_url,
has_bearer_token=bool(session.get('direct_bearer_token'))
)
@import_bp.route('/direct-auth', methods=['GET', 'POST'])
def direct_spotify_auth():
"""
Allow users to manually enter a Spotify bearer token for direct API access.
This bypasses the OAuth flow and is useful when API limitations are in place.
"""
error = None
success = None
if request.method == 'POST':
bearer_token = request.form.get('bearer_token')
if bearer_token:
try:
# Store the token in session
session['direct_bearer_token'] = bearer_token
# Test the token with a simple request
from musicround.helpers.spotify_direct import SpotifyDirectClient
client = SpotifyDirectClient(bearer_token=bearer_token)
# Try to get current user info as a test
result = client._make_api_request("me")
if result and 'id' in result:
session['direct_spotify_user'] = result['id']
session['direct_spotify_username'] = result.get('display_name', result['id'])
success = f"Successfully authenticated as {session['direct_spotify_username']}"
else:
error = "Token validation failed. Please check the token and try again."
except Exception as e:
current_app.logger.error(f"Error validating bearer token: {e}")
error = f"Error: {str(e)}"
else:
error = "No bearer token provided"
# Get stored user info if available
spotify_user = session.get('direct_spotify_user')
spotify_username = session.get('direct_spotify_username')
return render_template(
'spotify_direct_auth.html',
error=error,
success=success,
spotify_user=spotify_user,
spotify_username=spotify_username
)
@import_bp.route('/update-direct-token', methods=['POST'])
def update_direct_token():
"""Update the direct bearer token and redirect back to the referring page"""
# Get return URL from form or default to playlist page
return_url = request.form.get('return_url') or url_for('import.direct_official_playlists')
# Check if clearing token was requested
if request.form.get('clear_token'):
session.pop('direct_bearer_token', None)
session.pop('direct_spotify_user', None)
session.pop('direct_spotify_username', None)
flash('Bearer token cleared successfully', 'success')
return redirect(return_url)
# Get bearer token from form
bearer_token = request.form.get('bearer_token')
if not bearer_token:
flash('No bearer token provided', 'warning')
return redirect(return_url)
try:
# Store the token in session
session['direct_bearer_token'] = bearer_token
# Test the token with a simple request
from musicround.helpers.spotify_direct import SpotifyDirectClient
client = SpotifyDirectClient(bearer_token=bearer_token)
# Try to get current user info as a test
result = client._make_api_request("me")
if result and 'id' in result:
session['direct_spotify_user'] = result['id']
session['direct_spotify_username'] = result.get('display_name', result['id'])
flash(f'Successfully authenticated as {session["direct_spotify_username"]}', 'success')
else:
flash('Token validation failed. Please check the token and try again.', 'error')
except Exception as e:
current_app.logger.error(f"Error validating bearer token: {e}")
flash(f'Error validating token: {str(e)}', 'error')
return redirect(return_url)
+111
View File
@@ -0,0 +1,111 @@
import random
import os
import requests
import json
from flask import Blueprint, session, redirect, request, render_template, url_for, current_app, flash
from musicround.models import Song, db
from musicround.helpers.metadata import get_song_metadata_by_isrc
from musicround.helpers.import_helper import ImportHelper
import_songs_bp = Blueprint('import_songs', __name__, url_prefix='/import')
# Legacy function retained for backward compatibility
def import_track(track_id):
"""Legacy helper function that now uses the new ImportHelper"""
result = ImportHelper.import_item('spotify', 'track', track_id)
return result['imported_count'] > 0
# Legacy function retained for backward compatibility
def import_pl(playlist_id):
"""Legacy helper function that now uses the new ImportHelper"""
ImportHelper.import_item('spotify', 'playlist', playlist_id)
# Legacy function retained for backward compatibility
def import_al(album_id):
"""Legacy helper function that now uses the new ImportHelper"""
ImportHelper.import_item('spotify', 'album', album_id)
@import_songs_bp.route('/song', methods=['GET', 'POST'])
def import_song():
if 'access_token' not in session:
return redirect(url_for('users.login'))
if request.method == 'POST':
track_id = request.form['song_id']
result = ImportHelper.import_item('spotify', 'track', track_id)
if result['imported_count'] > 0:
flash(f'Successfully imported {result["imported_count"]} song!', 'success')
elif result['skipped_count'] > 0:
flash('Song was already in the database.', 'info')
else:
flash(f'Error importing song: {", ".join(result["errors"])}', 'danger')
return redirect(url_for('core.view_songs'))
return render_template('service_import.html',
service_name='Spotify',
item_type='Track',
url_example_prefix='https://open.spotify.com/track/',
url_example_id='6rqhFgbbKwnb9MLmUQDhG6',
id_field='song_id',
form_action=url_for('import_songs.import_song'),
back_url=url_for('core.search'))
@import_songs_bp.route('/playlist', methods=['GET', 'POST'])
def import_playlist():
if 'access_token' not in session:
return redirect(url_for('users.login'))
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 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')
return redirect(url_for('core.view_songs'))
return render_template('service_import.html',
service_name='Spotify',
item_type='Playlist',
url_example_prefix='https://open.spotify.com/playlist/',
url_example_id='37i9dQZF1DXcBWIGoYBM5M',
id_field='playlist_id',
form_action=url_for('import_songs.import_playlist'),
back_url=url_for('core.search'))
@import_songs_bp.route('/album', methods=['GET', 'POST'])
def import_album():
if 'access_token' not in session:
return redirect(url_for('users.login'))
if request.method == 'POST':
album_id = request.form['album_id']
result = ImportHelper.import_item('spotify', 'album', album_id)
if result['imported_count'] > 0:
flash(f'Successfully imported {result["imported_count"]} songs from album!', '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 album: {", ".join(result["errors"])}', 'danger')
return redirect(url_for('core.view_songs'))
return render_template('service_import.html',
service_name='Spotify',
item_type='Album',
url_example_prefix='https://open.spotify.com/album/',
url_example_id='4aawyAB9vmqN3uQ7FjRGTy',
id_field='album_id',
form_action=url_for('import_songs.import_album'),
back_url=url_for('core.search'))
+21
View File
@@ -0,0 +1,21 @@
from flask import Blueprint, session, redirect, url_for, jsonify, request, current_app
import base64
process_bp = Blueprint('process', __name__, url_prefix='/process')
@process_bp.route('/base64', methods=['POST'])
def base64_encode_data():
"""
Return base64-encoded string from data provided in request body.
"""
if 'access_token' not in session:
return redirect(url_for('users.login')) # Assuming 'users.login' is the correct endpoint
# Get binary data from request
data = request.get_data()
if not data:
return jsonify({'error': 'No data provided'}), 400
return jsonify({
'encoded': base64.b64encode(data).decode('utf-8')
})
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff