diff --git a/app/jellyfin_routes.py b/app/jellyfin_routes.py deleted file mode 100644 index 74c3ba5..0000000 --- a/app/jellyfin_routes.py +++ /dev/null @@ -1,228 +0,0 @@ -import time -from flask import 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 Playlist,Track, playlist_tracks -from spotipy.exceptions import SpotifyException - - -from jellyfin.objects import PlaylistMetadata - - - -@app.route('/jellyfin_playlists') -@functions.jellyfin_login_required -def jellyfin_playlists(): - try: - # Fetch playlists from Jellyfin - playlists = jellyfin.get_playlists(session_token=functions._get_token_from_sessioncookie()) - spotify_data = {'playlists': {'items': []}} - - # Extract Spotify playlist IDs from the database - for pl in playlists: - # Retrieve the playlist from the database using Jellyfin ID - from_db = Playlist.query.filter_by(jellyfin_id=pl['Id']).first() - playlist_data = None - not_found = False - if from_db and from_db.provider_playlist_id: - pl_id = from_db.provider_playlist_id - try: - playlist_data = functions.get_cached_spotify_playlist(pl_id) - - except SpotifyException as e: - app.logger.error(f"Error Fetching Playlist {pl_id}: {e}") - not_found = 'http status: 404' in str(e) - if not_found: - playlist_data = { - 'status':'red', - 'description': 'Playlist has most likely been removed. You can keep it, but won´t receive Updates.', - 'id': from_db.provider_playlist_id, - 'name' : from_db.name - - } - - if playlist_data: - spotify_data['playlists']['items'].append(playlist_data) - - else: - app.logger.warning(f"No database entry found for Jellyfin playlist ID: {pl['Id']}") - - prepared_data = functions.prepPlaylistData(spotify_data) - - return render_template('jellyfin_playlists.html', playlists=prepared_data) - except SpotifyException as e: - app.logger.error(f"Error fetching monitored playlists: {e}") - error_data, error_message = e, f'Could not retrieve monitored Playlists. Please try again later. This is most likely due to an Error in the Spotify API or an rate limit has been reached.' - return render_template('jellyfin_playlists.html', playlists=functions.prepPlaylistData({'playlists': {'items': []}}), error_message=error_message,error_data = error_data) - - except Exception as e: - app.logger.error(f"Error in /jellyfin_playlists route: {str(e)}") - flash('An error occurred while fetching playlists.', 'danger') - return render_template('jellyfin_playlists.html', playlists=functions.prepPlaylistData({'playlists': {'items': []}}), error_message='An error occurred while fetching playlists.',error_data = e) - - -@app.route('/addplaylist', methods=['POST']) -@functions.jellyfin_login_required -def add_playlist(): - playlist_id = request.form.get('item_id') # HTMX sends the form data - playlist_name = request.form.get('item_name') # Optionally retrieve playlist name from the form - if not playlist_id: - flash('No playlist ID provided') - return '' - - try: - # Fetch playlist from Spotify API (or any relevant API) - playlist_data = functions.get_cached_spotify_playlist(playlist_id) - - # Check if playlist already exists in the database - playlist = Playlist.query.filter_by(provider_playlist_id=playlist_id).first() - - if not playlist: - # Add new playlist if it doesn't exist - # create the playlist via api key, with the first admin as 'owner' - 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 = playlist_data['tracks']['total'], tracks_available=0, jellyfin_id = fromJellyfin) - 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 = functions._get_logged_in_user() - playlist.tracks_available = 0 - - spotify_tracks = {} - offset = 0 - while True: - playlist_items = sp.playlist_items(playlist.provider_playlist_id, offset=offset, limit=100) - items = playlist_items['items'] - spotify_tracks.update({offset + idx: track['track'] for idx, track in enumerate(items) if track['track']}) - - if len(items) < 100: # No more tracks to fetch - break - offset += 100 # Move to the next batch - for idx, track_data in spotify_tracks.items(): - track_info = track_data - if not track_info: - continue - track = Track.query.filter_by(provider_track_id=track_info['id']).first() - - if not track: - # Add new track if it doesn't exist - track = Track(name=track_info['name'], provider_track_id=track_info['id'], provider_uri=track_info['uri'], downloaded=False) - 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/', 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/', 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/') -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') - spotify_id = request.args.get('spotify_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,spotify_id= spotify_id,search_query = search_query) - return jsonify({'error': 'No search query provided'}), 400 \ No newline at end of file diff --git a/app/routes.py b/app/routes.py deleted file mode 100644 index fcf0cb1..0000000 --- a/app/routes.py +++ /dev/null @@ -1,355 +0,0 @@ -import json -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.models import JellyfinUser,Playlist,Track -from celery.result import AsyncResult - -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 - -pl_bp = Blueprint('playlist', __name__) -@pl_bp.before_request -def set_active_provider(): - """ - Middleware to select the active provider based on request parameters. - """ - 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 - -@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) - - -# 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(): - unlinked_tracks = Track.query.filter_by(downloaded=True,jellyfin_id=None).all() - tracks = [] - for ult in unlinked_tracks: - sp_track = functions.get_cached_spotify_track(ult.provider_track_id) - duration_ms = sp_track['duration_ms'] - minutes = duration_ms // 60000 - seconds = (duration_ms % 60000) // 1000 - tracks.append({ - 'title': sp_track['name'], - 'artist': ', '.join([artist['name'] for artist in sp_track['artists']]), - 'url': sp_track['external_urls']['spotify'], - 'duration': f'{minutes}:{seconds:02d}', - 'preview_url': sp_track['preview_url'], - 'downloaded': ult.downloaded, - 'filesystem_path': ult.filesystem_path, - 'jellyfin_id': ult.jellyfin_id, - 'spotify_id': sp_track['id'], - 'duration_ms': duration_ms, - 'download_status' : ult.download_status - }) - - return render_template('admin/link_issues.html' , tracks = tracks ) - - - -@app.route('/run_task/', 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('/add_single',methods=['GET']) -@functions.jellyfin_login_required -def add_single(): - playlist = request.args.get('playlist') - error = None - errdata= None - if playlist: - parsed = sp._get_id(type='playlist',id=playlist) - if parsed: - try: - functions.get_cached_spotify_playlist(parsed) - - return redirect(f'/playlist/view/{parsed}') - except SpotifyException as e: - url_match = re.search(sp._regex_spotify_url, playlist) - if url_match is not None: - resp = functions.fetch_spotify_playlist(playlist,None) - parsed_data = functions.parse_spotify_playlist_html(resp) - error = (f'Playlist can´t be fetched') - - errdata = str(e) - - return render_template('index.html',error_message = error, error_data = errdata) - - - - -@app.route('/playlists') -@app.route('/categories') -@app.route('/playlists/monitored') -@functions.jellyfin_login_required -def loaditems(): - country = app.config['SPOTIFY_COUNTRY_CODE'] - offset = int(request.args.get('offset', 0)) # Get the offset (default to 0 for initial load) - limit = 20 # Define a limit for pagination - additional_query = '' - items_subtitle = '' - error_message = None # Placeholder for error messages - error_data = '' - if request.path == '/playlists/monitored': - try: - db_playlists = db.session.query(Playlist).offset(offset).limit(limit).all() - max_items = db.session.query(Playlist).count() - - provider_playlist_ids = [playlist.provider_playlist_id for playlist in db_playlists] - spotify_data = functions.get_cached_spotify_playlists(tuple(provider_playlist_ids)) - for x in spotify_data['playlists']['items']: - for from_db in db_playlists: - if x['id'] == from_db.provider_playlist_id: - x['name'] = from_db.name - data = functions.prepPlaylistData(spotify_data) - items_title = "Monitored Playlists" - items_subtitle = "These playlists are already monitored by the Server. If you add one to your Jellyfin account, they will be available immediately." - except SpotifyException as e: - app.logger.error(f"Error fetching monitored playlists: {e}") - data, max_items, items_title = [], e, f'Could not retrieve monitored Playlists. Please try again later. This is most likely due to an Error in the Spotify API or an rate limit has been reached.' - error_message = items_title - error_data = max_items - - elif request.path == '/playlists': - cat = request.args.get('cat', None) - if cat is not None: - data, max_items, items_title = functions.getCategoryPlaylists(category=cat, offset=offset) - if not data: # Check if data is empty - error_message = items_title # Set the error message from the function - error_data = max_items - additional_query += f"&cat={cat}" - else: - data, max_items, items_title = functions.getFeaturedPlaylists(country=country, offset=offset) - if not data: # Check if data is empty - error_message = items_title # Set the error message from the function - error_data = max_items - - elif request.path == '/categories': - try: - data, max_items, items_title = functions.getCategories(country=country, offset=offset) - except Exception as e: - app.logger.error(f"Error fetching categories: {e}") - data, max_items, items_title = [], e, f'Error: Could not load categories. Please try again later. ' - error_message = items_title - error_data = max_items - - - next_offset = offset + len(data) - total_items = max_items - context = { - 'items': data, - 'next_offset': next_offset, - 'total_items': total_items, - 'endpoint': request.path, - 'items_title': items_title, - 'items_subtitle': items_subtitle, - 'additional_query': additional_query, - 'error_message': error_message, # Pass error message to the template - 'error_data': error_data, # Pass error message to the template - } - - if request.headers.get('HX-Request'): # Check if the request is from HTMX - return render_template('partials/_spotify_items.html', **context) - else: - return render_template('items.html', **context) - - -@app.route('/search') -@functions.jellyfin_login_required -def searchResults(): - query = request.args.get('query') - context = {} - if query: - # Add your logic here to perform the search on Spotify (or Jellyfin) - search_result = sp.search(q = query, type= 'playlist',limit= 50, market=app.config['SPOTIFY_COUNTRY_CODE']) - context = { - #'artists' : functions.prepArtistData(search_result ), - 'playlists' : functions.prepPlaylistData(search_result ), - #'albums' : functions.prepAlbumData(search_result ), - 'query' : query - } - return render_template('search.html', **context) - else: - return render_template('search.html', query=None, results={}) - - -@pl_bp.route('/playlist/view/') -@functions.jellyfin_login_required -def get_playlist_tracks(playlist_id): - provider: MusicProviderClient = g.music_provider # Explicit type hint for g.music_provider - playlist: base.Playlist = provider.get_playlist(playlist_id) - tracks = functions.get_tracks_for_playlist(playlist.tracks) # Deine Funktion, um Tracks zu holen - # Berechne die gesamte Dauer der Playlist - total_duration_ms = sum([track['track']['duration_ms'] for track in data['tracks']['items'] if track['track']]) - - # Konvertiere die Gesamtdauer in ein lesbares Format - hours, remainder = divmod(total_duration_ms // 1000, 3600) - minutes, seconds = divmod(remainder, 60) - - # Formatierung der Dauer - 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(data['tracks']), - playlist_name=data['name'], - playlist_cover=data['images'][0]['url'], - playlist_description=data['description'], - last_updated = data['prepped_data'][0]['last_updated'], - last_changed = data['prepped_data'][0]['last_changed'], - item = data['prepped_data'][0], - - ) -@app.route('/associate_track', methods=['POST']) -@functions.jellyfin_login_required -def associate_track(): - jellyfin_id = request.form.get('jellyfin_id') - spotify_id = request.form.get('spotify_id') - - if not jellyfin_id or not spotify_id: - flash('Missing Jellyfin or Spotify ID') - - # Retrieve the track by Spotify ID - track = Track.query.filter_by(provider_track_id=spotify_id).first() - - if not track: - flash('Track not found') - return '' - - # Associate the Jellyfin ID with the track - track.jellyfin_id = jellyfin_id - - 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 '' - - -@app.route('/test') -def test(): - playlist_id = "37i9dQZF1DX12qgyzUprB6" - client = SpotifyClient(cookie_file='/jellyplist/open.spotify.com_cookies.txt') - client.authenticate() - pl = client.get_playlist(playlist_id=playlist_id) - browse = client.browse_all() - page = client.browse_page(browse[0].items[12]) - return '' \ No newline at end of file diff --git a/templates/partials/_spotify_item.html b/templates/partials/_spotify_item.html deleted file mode 100644 index 0ffd45d..0000000 --- a/templates/partials/_spotify_item.html +++ /dev/null @@ -1,49 +0,0 @@ -
-
- - - {% if item.status %} - - {% if item.track_count > 0 %} - {{ item.tracks_available }} / {{ item.tracks_linked}} / {{ item.track_count}} - {% else %} - not Available - {% endif %} - - {% endif %} - - -
- {{ item.name }} -
- - -
-
-
{{ item.name }}
-

{{ item.description }}

-
-
- {% if item.type == 'category'%} - - - - {%else%} - - - - - {%endif%} - {% include 'partials/_add_remove_button.html' %} -
-
- -
-
\ No newline at end of file diff --git a/templates/partials/_spotify_items.html b/templates/partials/_spotify_items.html deleted file mode 100644 index 896008b..0000000 --- a/templates/partials/_spotify_items.html +++ /dev/null @@ -1,24 +0,0 @@ -{% for item in items %} - {% include 'partials/_spotify_item.html' %} - -{% endfor %} - -{% if next_offset < total_items %} -
- Loading more items... -
-{% endif %} - - \ No newline at end of file diff --git a/templates/partials/_toast.html b/templates/partials/_toast.html deleted file mode 100644 index fa4e494..0000000 --- a/templates/partials/_toast.html +++ /dev/null @@ -1,18 +0,0 @@ - - - \ No newline at end of file