added blueprint and restructured existing routes

This commit is contained in:
Kamil
2024-12-03 12:38:47 +00:00
parent d5aee793a0
commit 3a26c054a0
4 changed files with 635 additions and 3 deletions

17
app/routes/__init__.py Normal file
View File

@@ -0,0 +1,17 @@
from flask import Blueprint, request, g
from app import app
from app.registry.music_provider_registry import MusicProviderRegistry
pl_bp = Blueprint('playlist', __name__)
@pl_bp.before_request
def set_active_provider():
"""
Middleware to select the active provider based on request parameters.
"""
app.logger.debug(f"Setting active provider: {request.args.get('provider', 'Spotify')}")
provider_id = request.args.get('provider', 'Spotify') # Default to Spotify
try:
g.music_provider = MusicProviderRegistry.get_provider(provider_id)
except ValueError as e:
return {"error": str(e)}, 400

View File

@@ -0,0 +1,202 @@
from collections import defaultdict
import time
from flask import Blueprint, Flask, jsonify, render_template, request, redirect, url_for, session, flash
from sqlalchemy import insert
from app import app, db, jellyfin, functions, device_id,sp
from app.models import JellyfinUser, Playlist,Track, playlist_tracks
from spotipy.exceptions import SpotifyException
from app.registry.music_provider_registry import MusicProviderRegistry
from jellyfin.objects import PlaylistMetadata
from app.routes import pl_bp
@app.route('/jellyfin_playlists')
@functions.jellyfin_login_required
def jellyfin_playlists():
playlists = jellyfin.get_playlists(session_token=functions._get_token_from_sessioncookie())
playlists_by_provider = defaultdict(list)
provider_playlists_data = {}
for pl in playlists:
from_db : Playlist | None = Playlist.query.filter_by(jellyfin_id=pl['Id']).first()
if from_db and from_db.provider_playlist_id:
pl_id = from_db.provider_playlist_id
playlists_by_provider[from_db.provider_id].append(from_db)
# 3. Fetch all Data from the provider using the get_playlist() method
for provider_id, playlists in playlists_by_provider.items():
try:
provider_client = MusicProviderRegistry.get_provider(provider_id)
except ValueError:
flash(f"Provider {provider_id} not found.", "error")
continue
combined_playlists = []
for pl in playlists:
provider_playlist = provider_client.get_playlist(pl.provider_playlist_id)
# 4. Convert the playlists to CombinedPlaylistData
combined_data = functions.prepPlaylistData(provider_playlist)
if combined_data:
combined_playlists.append(combined_data)
provider_playlists_data[provider_id] = combined_playlists
# 5. Display the resulting Groups in a template called 'monitored_playlists.html', one Heading per Provider
return render_template('monitored_playlists.html', provider_playlists_data=provider_playlists_data,title="Jellyfin Playlists" , subtitle="Playlists you have added to Jellyfin")
@pl_bp.route('/addplaylist', methods=['POST'])
@functions.jellyfin_login_required
def add_playlist():
playlist_id = request.form.get('item_id')
playlist_name = request.form.get('item_name')
# also get the provider id from the query params
provider_id = request.args.get('provider')
if not playlist_id:
flash('No playlist ID provided')
return ''
# if no provider_id is provided, then show an error and return an empty string
if not provider_id:
flash('No provider ID provided')
return ''
try:
# get the playlist from the correct provider
provider_client = MusicProviderRegistry.get_provider(provider_id)
playlist_data = provider_client.get_playlist(playlist_id)
# Check if playlist already exists in the database, using the provider_id and the provider_playlist_id
playlist = Playlist.query.filter_by(provider_playlist_id=playlist_id, provider_id=provider_id).first()
# Add new playlist in the database if it doesn't exist
# create the playlist via api key, with the first admin as 'owner'
if not playlist:
fromJellyfin = jellyfin.create_music_playlist(functions._get_api_token(),playlist_data.name,[],functions._get_admin_id())['Id']
playlist = Playlist(name=playlist_data.name, provider_playlist_id=playlist_id,provider_uri=playlist_data.uri,track_count = len(playlist_data.tracks), tracks_available=0, jellyfin_id = fromJellyfin, provider_id=provider_id)
db.session.add(playlist)
db.session.commit()
if app.config['START_DOWNLOAD_AFTER_PLAYLIST_ADD']:
functions.manage_task('download_missing_tracks')
# Get the logged-in user
user : JellyfinUser = functions._get_logged_in_user()
playlist.tracks_available = 0
for idx, track_data in enumerate(playlist_data.tracks):
track = Track.query.filter_by(provider_track_id=track_data.track.id, provider_id=provider_id).first()
if not track:
# Add new track if it doesn't exist
track = Track(name=track_data.track.name, provider_track_id=track_data.track.id, provider_uri=track_data.track.uri, downloaded=False,provider_id = provider_id)
db.session.add(track)
db.session.commit()
elif track.downloaded:
playlist.tracks_available += 1
db.session.commit()
# Add track to playlist with order if it's not already associated
if track not in playlist.tracks:
# Insert into playlist_tracks with track order
stmt = insert(playlist_tracks).values(
playlist_id=playlist.id,
track_id=track.id,
track_order=idx # Maintain the order of tracks
)
db.session.execute(stmt)
db.session.commit()
functions.update_playlist_metadata(playlist,playlist_data)
if playlist not in user.playlists:
user.playlists.append(playlist)
db.session.commit()
jellyfin.add_users_to_playlist(session_token=functions._get_api_token(), user_id=functions._get_admin_id(),playlist_id = playlist.jellyfin_id,user_ids= [user.jellyfin_user_id])
flash(f'Playlist "{playlist_data.name}" successfully added','success')
else:
flash(f'Playlist "{playlist_data.name}" already in your list')
item = {
"name" : playlist_data.name,
"id" : playlist_id,
"can_add":False,
"can_remove":True,
"jellyfin_id" : playlist.jellyfin_id
}
return render_template('partials/_add_remove_button.html',item= item)
except Exception as e:
flash(str(e))
return ''
@app.route('/delete_playlist/<playlist_id>', methods=['DELETE'])
@functions.jellyfin_login_required
def delete_playlist(playlist_id):
# Logic to delete the playlist using JellyfinClient
try:
user = functions._get_logged_in_user()
for pl in user.playlists:
if pl.jellyfin_id == playlist_id:
user.playlists.remove(pl)
playlist = pl
jellyfin.remove_user_from_playlist(session_token= functions._get_api_token(), playlist_id= playlist_id, user_id=user.jellyfin_user_id)
db.session.commit()
flash('Playlist removed')
item = {
"name" : playlist.name,
"id" : playlist.provider_playlist_id,
"can_add":True,
"can_remove":False,
"jellyfin_id" : playlist.jellyfin_id
}
return render_template('partials/_add_remove_button.html',item= item)
except Exception as e:
flash(f'Failed to remove item: {str(e)}')
@app.route('/wipe_playlist/<playlist_id>', methods=['DELETE'])
@functions.jellyfin_admin_required
def wipe_playlist(playlist_id):
playlist = Playlist.query.filter_by(jellyfin_id=playlist_id).first()
name = ""
id = ""
jf_id = ""
try:
jellyfin.remove_item(session_token=functions._get_api_token(), playlist_id=playlist_id)
except Exception as e:
flash(f"Jellyfin API Error: {str(e)}")
if playlist:
# Delete the playlist
name = playlist.name
id = playlist.provider_playlist_id
jf_id = playlist.jellyfin_id
db.session.delete(playlist)
db.session.commit()
flash('Playlist Deleted', category='info')
item = {
"name" : name,
"id" : id,
"can_add":True,
"can_remove":False,
"jellyfin_id" : jf_id
}
return render_template('partials/_add_remove_button.html',item= item)
@functions.jellyfin_login_required
@app.route('/get_jellyfin_stream/<string:jellyfin_id>')
def get_jellyfin_stream(jellyfin_id):
user_id = session['jellyfin_user_id'] # Beispiel: dynamischer Benutzer
api_key = functions._get_token_from_sessioncookie() # Beispiel: dynamischer API-Schlüssel
stream_url = f"{app.config['JELLYFIN_SERVER_URL']}/Audio/{jellyfin_id}/universal?UserId={user_id}&DeviceId={device_id}&MaxStreamingBitrate=140000000&Container=opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg&TranscodingContainer=mp4&TranscodingProtocol=hls&AudioCodec=aac&api_key={api_key}&PlaySessionId={int(time.time())}&StartTimeTicks=0&EnableRedirection=true&EnableRemoteMedia=false"
return jsonify({'stream_url': stream_url})
@app.route('/search_jellyfin', methods=['GET'])
@functions.jellyfin_login_required
def search_jellyfin():
search_query = request.args.get('search_query')
provider_track_id = request.args.get('provider_track_id')
if search_query:
results = jellyfin.search_music_tracks(functions._get_token_from_sessioncookie(), search_query)
# Render only the search results section as response
return render_template('partials/_jf_search_results.html', results=results,provider_track_id= provider_track_id,search_query = search_query)
return jsonify({'error': 'No search query provided'}), 400

412
app/routes/routes.py Normal file
View File

@@ -0,0 +1,412 @@
import json
import os
import re
from flask import Flask, Response, jsonify, render_template, request, redirect, url_for, session, flash, Blueprint, g
from app import app, db, functions, sp, jellyfin, celery, jellyfin_admin_token, jellyfin_admin_id,device_id, cache, read_dev_build_file, tasks
from app.classes import AudioProfile, CombinedPlaylistData
from app.models import JellyfinUser,Playlist,Track
from celery.result import AsyncResult
from typing import List
from app.providers import base
from app.providers.base import MusicProviderClient
from app.providers.spotify import SpotifyClient
from app.registry.music_provider_registry import MusicProviderRegistry
from ..version import __version__
from spotipy.exceptions import SpotifyException
from collections import defaultdict
from app.routes import pl_bp
@app.context_processor
def add_context():
unlinked_track_count = len(Track.query.filter_by(downloaded=True,jellyfin_id=None).all())
version = f"v{__version__}{read_dev_build_file()}"
return dict(unlinked_track_count = unlinked_track_count, version = version, config = app.config , registered_providers = MusicProviderRegistry.list_providers())
# this feels wrong
skip_endpoints = ['task_status']
@app.after_request
def render_messages(response: Response) -> Response:
if request.headers.get("HX-Request"):
if request.endpoint not in skip_endpoints:
messages = render_template("partials/alerts.jinja2")
response.headers['HX-Trigger'] = 'showToastMessages'
response.data = response.data + messages.encode("utf-8")
return response
@app.route('/admin/tasks')
@functions.jellyfin_admin_required
def task_manager():
statuses = {}
for task_name, task_id in functions.TASK_STATUS.items():
if task_id:
result = AsyncResult(task_id)
statuses[task_name] = {'state': result.state, 'info': result.info if result.info else {}}
else:
statuses[task_name] = {'state': 'NOT STARTED', 'info': {}}
return render_template('admin/tasks.html', tasks=statuses,lock_keys = functions.LOCK_KEYS)
@app.route('/admin')
@app.route('/admin/link_issues')
@functions.jellyfin_admin_required
def link_issues():
# add the ability to pass a query parameter to dislplay even undownloaded tracks
list_undownloaded = request.args.get('list_undownloaded')
if list_undownloaded:
unlinked_tracks = Track.query.filter_by(jellyfin_id=None).all()
else:
unlinked_tracks = Track.query.filter_by(downloaded=True,jellyfin_id=None).all()
tracks = []
for ult in unlinked_tracks:
provider_track = functions.get_cached_provider_track(ult.provider_track_id, ult.provider_id)
duration_ms = provider_track.duration_ms
minutes = duration_ms // 60000
seconds = (duration_ms % 60000) // 1000
tracks.append({
'title': provider_track.name,
'artist': ', '.join([artist.name for artist in provider_track.artists]),
'url': provider_track.external_urls,
'duration': f'{minutes}:{seconds:02d}',
'preview_url': '',
'downloaded': ult.downloaded,
'filesystem_path': ult.filesystem_path,
'jellyfin_id': ult.jellyfin_id,
'provider_track_id': provider_track.id,
'duration_ms': duration_ms,
'download_status' : ult.download_status,
'provider_id' : ult.provider_id
})
return render_template('admin/link_issues.html' , tracks = tracks )
@app.route('/run_task/<task_name>', methods=['POST'])
@functions.jellyfin_admin_required
def run_task(task_name):
status, info = functions.manage_task(task_name)
# Rendere nur die aktualisierte Zeile der Task
task_info = {task_name: {'state': status, 'info': info}}
return render_template('partials/_task_status.html', tasks=task_info)
@app.route('/task_status')
@functions.jellyfin_admin_required
def task_status():
statuses = {}
for task_name, task_id in functions.TASK_STATUS.items():
if task_id:
result = AsyncResult(task_id)
statuses[task_name] = {'state': result.state, 'info': result.info if result.info else {}}
else:
statuses[task_name] = {'state': 'NOT STARTED', 'info': {}}
# Render the HTML partial template instead of returning JSON
return render_template('partials/_task_status.html', tasks=statuses)
@app.route('/')
@functions.jellyfin_login_required
def index():
users = JellyfinUser.query.all()
return render_template('index.html', user=session['jellyfin_user_name'], users=users)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
try:
jellylogin = jellyfin.login_with_password(username=username, password=password)
if jellylogin:
session['jellyfin_access_token'], session['jellyfin_user_id'], session['jellyfin_user_name'],session['is_admin'] = jellylogin
session['debug'] = app.debug
# Check if the user already exists
user = JellyfinUser.query.filter_by(jellyfin_user_id=session['jellyfin_user_id']).first()
if not user:
# Add the user to the database if they don't exist
new_user = JellyfinUser(name=session['jellyfin_user_name'], jellyfin_user_id=session['jellyfin_user_id'], is_admin = session['is_admin'])
db.session.add(new_user)
db.session.commit()
return redirect('/')
except:
flash('Login failed. Please check your Jellyfin credentials and try again.', 'error')
return redirect(url_for('login'))
return render_template('login.html')
@app.route('/logout')
def logout():
session.pop('jellyfin_user_name', None)
session.pop('jellyfin_access_token', None)
return redirect(url_for('login'))
@app.route('/playlist/open',methods=['GET'])
@functions.jellyfin_login_required
def openPlaylist():
playlist = request.args.get('playlist')
error = None
errdata= None
if playlist:
for provider_id in MusicProviderRegistry.list_providers():
try:
provider_client = MusicProviderRegistry.get_provider(provider_id)
extracted_playlist_id = provider_client.extract_playlist_id(playlist)
provider_playlist = provider_client.get_playlist(extracted_playlist_id)
combined_data = functions.prepPlaylistData(provider_playlist)
if combined_data:
# If the playlist is found, redirect to the playlist view, but also include the provider ID in the URL
return redirect(url_for('playlist.get_playlist_tracks', playlist_id=extracted_playlist_id, provider=provider_id))
except Exception as e:
error = f"Error fetching playlist from {provider_id}: {str(e)}"
errdata = e
return render_template('index.html',error_message = error, error_data = errdata)
@pl_bp.route('/browse')
@functions.jellyfin_login_required
def browse():
provider: MusicProviderClient = g.music_provider
browse_data = provider.browse()
return render_template('browse.html', browse_data=browse_data,provider_id=provider._identifier)
@pl_bp.route('/browse/page/<page_id>')
@functions.jellyfin_login_required
def browse_page(page_id):
provider: MusicProviderClient = g.music_provider
combined_playlist_data : List[CombinedPlaylistData] = []
data = provider.browse_page(page_id)
for item in data:
cpd = functions.prepPlaylistData(item)
if cpd:
combined_playlist_data.append(cpd)
return render_template('browse_page.html', data=combined_playlist_data,provider_id=provider._identifier)
@pl_bp.route('/playlists/monitored')
@functions.jellyfin_login_required
def monitored_playlists():
# 1. Get all Playlists from the Database.
all_playlists = Playlist.query.all()
# 2. Group them by provider
playlists_by_provider = defaultdict(list)
for playlist in all_playlists:
playlists_by_provider[playlist.provider_id].append(playlist)
provider_playlists_data = {}
# 3. Fetch all Data from the provider using the get_playlist() method
for provider_id, playlists in playlists_by_provider.items():
try:
provider_client = MusicProviderRegistry.get_provider(provider_id)
except ValueError:
flash(f"Provider {provider_id} not found.", "error")
continue
combined_playlists = []
for pl in playlists:
provider_playlist = provider_client.get_playlist(pl.provider_playlist_id)
# 4. Convert the playlists to CombinedPlaylistData
combined_data = functions.prepPlaylistData(provider_playlist)
if combined_data:
combined_playlists.append(combined_data)
provider_playlists_data[provider_id] = combined_playlists
# 5. Display the resulting Groups in a template called 'monitored_playlists.html', one Heading per Provider
return render_template('monitored_playlists.html', provider_playlists_data=provider_playlists_data, title="Monitored Playlists", subtitle="Playlists which are already monitored by Jellyplist and are available immediately")
@app.route('/search')
@functions.jellyfin_login_required
def searchResults():
query = request.args.get('query')
context = {}
if query:
#iterate through every registered music provider and perform the search with it.
# Group the results by provider and display them using monitorerd_playlists.html
search_results = defaultdict(list)
for provider_id in MusicProviderRegistry.list_providers():
try:
provider_client = MusicProviderRegistry.get_provider(provider_id)
results = provider_client.search_playlist(query)
for result in results:
search_results[provider_id].append(result)
except Exception as e:
flash(f"Error fetching search results from {provider_id}: {str(e)}", "error")
# the grouped search results, must be prepared using the prepPlaylistData function
for provider_id, playlists in search_results.items():
combined_playlists = []
for pl in playlists:
combined_data = functions.prepPlaylistData(pl)
if combined_data:
combined_playlists.append(combined_data)
search_results[provider_id] = combined_playlists
context['provider_playlists_data'] = search_results
context['title'] = 'Search Results'
context['subtitle'] = 'Search results from all providers'
return render_template('monitored_playlists.html', **context)
@pl_bp.route('/track_details/<track_id>')
@functions.jellyfin_login_required
def track_details(track_id):
provider_id = request.args.get('provider')
if not provider_id:
return jsonify({'error': 'Provider not specified'}), 400
track = Track.query.filter_by(provider_track_id=track_id, provider_id=provider_id).first()
if not track:
return jsonify({'error': 'Track not found'}), 404
provider_track = functions.get_cached_provider_track(track.provider_track_id, track.provider_id)
# query also this track using the jellyfin id directly from jellyfin
if track.jellyfin_id:
jellyfin_track = jellyfin.get_item(session_token=functions._get_api_token(), item_id=track.jellyfin_id)
if jellyfin_track:
jellyfin_filesystem_path = jellyfin_track['Path']
duration_ms = provider_track.duration_ms
minutes = duration_ms // 60000
seconds = (duration_ms % 60000) // 1000
track_details = {
'title': provider_track.name,
'artist': ', '.join([artist.name for artist in provider_track.artists]),
'url': provider_track.external_urls,
'duration': f'{minutes}:{seconds:02d}',
'downloaded': track.downloaded,
'filesystem_path': track.filesystem_path,
'jellyfin_id': track.jellyfin_id,
'provider_track_id': provider_track.id,
'provider_track_url': provider_track.external_urls[0].url if provider_track.external_urls else None,
'duration_ms': duration_ms,
'download_status': track.download_status,
'provider_id': track.provider_id,
'jellyfin_filesystem_path': jellyfin_filesystem_path if track.jellyfin_id else None,
}
return render_template('partials/track_details.html', track=track_details)
@pl_bp.route('/playlist/view/<playlist_id>')
@functions.jellyfin_login_required
def get_playlist_tracks(playlist_id):
provider: MusicProviderClient = g.music_provider
playlist: base.Playlist = provider.get_playlist(playlist_id)
tracks = functions.get_tracks_for_playlist(playlist.tracks, provider_id=provider._identifier)
total_duration_ms = sum([track.duration_ms for track in tracks])
# Convert the total duration to a readable format
hours, remainder = divmod(total_duration_ms // 1000, 3600)
minutes, seconds = divmod(remainder, 60)
# Format the duration
if hours > 0:
total_duration = f"{hours}h {minutes}min"
else:
total_duration = f"{minutes}min"
return render_template(
'tracks_table.html',
tracks=tracks,
total_duration=total_duration,
track_count=len(tracks),
provider_id = provider._identifier,
item=functions.prepPlaylistData(playlist),
)
@app.route('/associate_track', methods=['POST'])
@functions.jellyfin_login_required
def associate_track():
jellyfin_id = request.form.get('jellyfin_id')
provider_track_id = request.form.get('provider_track_id')
if not jellyfin_id or not provider_track_id:
flash('Missing Jellyfin or Spotify ID')
# Retrieve the track by Spotify ID
track = Track.query.filter_by(provider_track_id=provider_track_id).first()
if not track:
flash('Track not found')
return ''
# Associate the Jellyfin ID with the track
track.jellyfin_id = jellyfin_id
track.downloaded = True
try:
# Commit the changes to the database
db.session.commit()
flash("Track associated","success")
return ''
except Exception as e:
db.session.rollback() # Roll back the session in case of an error
flash(str(e))
return ''
@app.route("/unlock_key",methods = ['POST'])
@functions.jellyfin_admin_required
def unlock_key():
key_name = request.form.get('inputLockKey')
if key_name:
tasks.release_lock(key_name)
flash(f'Lock {key_name} released', category='success')
return ''
@pl_bp.route('/test')
def test():
#return ''
app.logger.info(f"performing full update on jellyfin track ids. (Update tracks and playlists if better quality will be found)")
downloaded_tracks : List[Track] = Track.query.all()
total_tracks = len(downloaded_tracks)
if not downloaded_tracks:
app.logger.info("No downloaded tracks without Jellyfin ID found.")
return {'status': 'No tracks to update'}
app.logger.info(f"Found {total_tracks} tracks to update ")
processed_tracks = 0
for track in downloaded_tracks:
try:
best_match = tasks.find_best_match_from_jellyfin(track)
if best_match:
track.downloaded = True
if track.jellyfin_id != best_match['Id']:
track.jellyfin_id = best_match['Id']
app.logger.info(f"Updated Jellyfin ID for track: {track.name} ({track.provider_track_id})")
if track.filesystem_path != best_match['Path']:
track.filesystem_path = best_match['Path']
app.logger.info(f"Updated filesystem_path for track: {track.name} ({track.provider_track_id})")
db.session.commit()
else:
app.logger.warning(f"No matching track found in Jellyfin for {track.name}.")
spotify_track = None
except Exception as e:
app.logger.error(f"Error searching Jellyfin for track {track.name}: {str(e)}")
processed_tracks += 1
progress = (processed_tracks / total_tracks) * 100
#self.update_state(state=f'{processed_tracks}/{total_tracks}: {track.name}', meta={'current': processed_tracks, 'total': total_tracks, 'percent': progress})
app.logger.info("Finished updating Jellyfin IDs for all tracks.")
return {'status': 'All tracks updated', 'total': total_tracks, 'processed': processed_tracks}