2.0 is coming
This commit is contained in:
@@ -1,25 +1,43 @@
|
||||
from flask import Blueprint, Response, request
|
||||
from flask import Blueprint, Response, request, jsonify
|
||||
import os
|
||||
import json
|
||||
import traceback
|
||||
import logging # Added logging import
|
||||
import uuid # For generating error task IDs
|
||||
import time # For timestamps
|
||||
from routes.utils.celery_queue_manager import download_queue_manager
|
||||
from routes.utils.celery_tasks import store_task_info, store_task_status, ProgressState # For error task creation
|
||||
import threading # For playlist watch trigger
|
||||
|
||||
playlist_bp = Blueprint('playlist', __name__)
|
||||
# Imports from playlist_watch.py
|
||||
from routes.utils.watch.db import (
|
||||
add_playlist_to_watch as add_playlist_db,
|
||||
remove_playlist_from_watch as remove_playlist_db,
|
||||
get_watched_playlist,
|
||||
get_watched_playlists,
|
||||
add_specific_tracks_to_playlist_table,
|
||||
remove_specific_tracks_from_playlist_table,
|
||||
is_track_in_playlist_db # Added import
|
||||
)
|
||||
from routes.utils.get_info import get_spotify_info # Already used, but ensure it's here
|
||||
from routes.utils.watch.manager import check_watched_playlists # For manual trigger
|
||||
|
||||
@playlist_bp.route('/download', methods=['GET'])
|
||||
def handle_download():
|
||||
logger = logging.getLogger(__name__) # Added logger initialization
|
||||
playlist_bp = Blueprint('playlist', __name__, url_prefix='/api/playlist')
|
||||
|
||||
@playlist_bp.route('/download/<playlist_id>', methods=['GET'])
|
||||
def handle_download(playlist_id):
|
||||
# Retrieve essential parameters from the request.
|
||||
url = request.args.get('url')
|
||||
name = request.args.get('name')
|
||||
artist = request.args.get('artist')
|
||||
orig_params = request.args.to_dict()
|
||||
orig_params["original_url"] = request.url
|
||||
|
||||
|
||||
# Construct the URL from playlist_id
|
||||
url = f"https://open.spotify.com/playlist/{playlist_id}"
|
||||
orig_params["original_url"] = url # Update original_url to the constructed one
|
||||
|
||||
# Validate required parameters
|
||||
if not url:
|
||||
if not url: # This check might be redundant now but kept for safety
|
||||
return Response(
|
||||
json.dumps({"error": "Missing required parameter: url"}),
|
||||
status=400,
|
||||
@@ -104,8 +122,23 @@ def get_playlist_info():
|
||||
|
||||
try:
|
||||
# Import and use the get_spotify_info function from the utility module.
|
||||
from routes.utils.get_info import get_spotify_info
|
||||
playlist_info = get_spotify_info(spotify_id, "playlist")
|
||||
|
||||
# If playlist_info is successfully fetched, check if it's watched
|
||||
# and augment track items with is_locally_known status
|
||||
if playlist_info and playlist_info.get('id'):
|
||||
watched_playlist_details = get_watched_playlist(playlist_info['id'])
|
||||
if watched_playlist_details: # Playlist is being watched
|
||||
if playlist_info.get('tracks') and playlist_info['tracks'].get('items'):
|
||||
for item in playlist_info['tracks']['items']:
|
||||
if item and item.get('track') and item['track'].get('id'):
|
||||
track_id = item['track']['id']
|
||||
item['track']['is_locally_known'] = is_track_in_playlist_db(playlist_info['id'], track_id)
|
||||
elif item and item.get('track'): # Track object exists but no ID
|
||||
item['track']['is_locally_known'] = False
|
||||
# If not watched, or no tracks, is_locally_known will not be added, or tracks won't exist to add it to.
|
||||
# Frontend should handle absence of this key as false.
|
||||
|
||||
return Response(
|
||||
json.dumps(playlist_info),
|
||||
status=200,
|
||||
@@ -121,3 +154,160 @@ def get_playlist_info():
|
||||
status=500,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
@playlist_bp.route('/watch/<string:playlist_spotify_id>', methods=['PUT'])
|
||||
def add_to_watchlist(playlist_spotify_id):
|
||||
"""Adds a playlist to the watchlist."""
|
||||
logger.info(f"Attempting to add playlist {playlist_spotify_id} to watchlist.")
|
||||
try:
|
||||
# Check if already watched
|
||||
if get_watched_playlist(playlist_spotify_id):
|
||||
return jsonify({"message": f"Playlist {playlist_spotify_id} is already being watched."}), 200
|
||||
|
||||
# Fetch playlist details from Spotify to populate our DB
|
||||
playlist_data = get_spotify_info(playlist_spotify_id, "playlist")
|
||||
if not playlist_data or 'id' not in playlist_data:
|
||||
logger.error(f"Could not fetch details for playlist {playlist_spotify_id} from Spotify.")
|
||||
return jsonify({"error": f"Could not fetch details for playlist {playlist_spotify_id} from Spotify."}), 404
|
||||
|
||||
add_playlist_db(playlist_data) # This also creates the tracks table
|
||||
|
||||
# REMOVED: Do not add initial tracks directly to DB.
|
||||
# The playlist watch manager will pick them up as new and queue downloads.
|
||||
# Tracks will be added to DB only after successful download via Celery task callback.
|
||||
# initial_track_items = playlist_data.get('tracks', {}).get('items', [])
|
||||
# if initial_track_items:
|
||||
# from routes.utils.watch.db import add_tracks_to_playlist_db # Keep local import for clarity
|
||||
# add_tracks_to_playlist_db(playlist_spotify_id, initial_track_items)
|
||||
|
||||
logger.info(f"Playlist {playlist_spotify_id} added to watchlist. Its tracks will be processed by the watch manager.")
|
||||
return jsonify({"message": f"Playlist {playlist_spotify_id} added to watchlist. Tracks will be processed shortly."}), 201
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding playlist {playlist_spotify_id} to watchlist: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Could not add playlist to watchlist: {str(e)}"}), 500
|
||||
|
||||
@playlist_bp.route('/watch/<string:playlist_spotify_id>/status', methods=['GET'])
|
||||
def get_playlist_watch_status(playlist_spotify_id):
|
||||
"""Checks if a specific playlist is being watched."""
|
||||
logger.info(f"Checking watch status for playlist {playlist_spotify_id}.")
|
||||
try:
|
||||
playlist = get_watched_playlist(playlist_spotify_id)
|
||||
if playlist:
|
||||
return jsonify({"is_watched": True, "playlist_data": playlist}), 200
|
||||
else:
|
||||
# Return 200 with is_watched: false, so frontend can clearly distinguish
|
||||
# between "not watched" and an actual error fetching status.
|
||||
return jsonify({"is_watched": False}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking watch status for playlist {playlist_spotify_id}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Could not check watch status: {str(e)}"}), 500
|
||||
|
||||
@playlist_bp.route('/watch/<string:playlist_spotify_id>', methods=['DELETE'])
|
||||
def remove_from_watchlist(playlist_spotify_id):
|
||||
"""Removes a playlist from the watchlist."""
|
||||
logger.info(f"Attempting to remove playlist {playlist_spotify_id} from watchlist.")
|
||||
try:
|
||||
if not get_watched_playlist(playlist_spotify_id):
|
||||
return jsonify({"error": f"Playlist {playlist_spotify_id} not found in watchlist."}), 404
|
||||
|
||||
remove_playlist_db(playlist_spotify_id)
|
||||
logger.info(f"Playlist {playlist_spotify_id} removed from watchlist successfully.")
|
||||
return jsonify({"message": f"Playlist {playlist_spotify_id} removed from watchlist."}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing playlist {playlist_spotify_id} from watchlist: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Could not remove playlist from watchlist: {str(e)}"}), 500
|
||||
|
||||
@playlist_bp.route('/watch/<string:playlist_spotify_id>/tracks', methods=['POST'])
|
||||
def mark_tracks_as_known(playlist_spotify_id):
|
||||
"""Fetches details for given track IDs and adds/updates them in the playlist's local DB table."""
|
||||
logger.info(f"Attempting to mark tracks as known for playlist {playlist_spotify_id}.")
|
||||
try:
|
||||
track_ids = request.json
|
||||
if not isinstance(track_ids, list) or not all(isinstance(tid, str) for tid in track_ids):
|
||||
return jsonify({"error": "Invalid request body. Expecting a JSON array of track Spotify IDs."}), 400
|
||||
|
||||
if not get_watched_playlist(playlist_spotify_id):
|
||||
return jsonify({"error": f"Playlist {playlist_spotify_id} is not being watched."}), 404
|
||||
|
||||
fetched_tracks_details = []
|
||||
for track_id in track_ids:
|
||||
try:
|
||||
track_detail = get_spotify_info(track_id, "track")
|
||||
if track_detail and track_detail.get('id'):
|
||||
fetched_tracks_details.append(track_detail)
|
||||
else:
|
||||
logger.warning(f"Could not fetch details for track {track_id} when marking as known for playlist {playlist_spotify_id}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch Spotify details for track {track_id}: {e}")
|
||||
|
||||
if not fetched_tracks_details:
|
||||
return jsonify({"message": "No valid track details could be fetched to mark as known.", "processed_count": 0}), 200
|
||||
|
||||
add_specific_tracks_to_playlist_table(playlist_spotify_id, fetched_tracks_details)
|
||||
logger.info(f"Successfully marked/updated {len(fetched_tracks_details)} tracks as known for playlist {playlist_spotify_id}.")
|
||||
return jsonify({"message": f"Successfully processed {len(fetched_tracks_details)} tracks for playlist {playlist_spotify_id}."}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking tracks as known for playlist {playlist_spotify_id}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Could not mark tracks as known: {str(e)}"}), 500
|
||||
|
||||
@playlist_bp.route('/watch/<string:playlist_spotify_id>/tracks', methods=['DELETE'])
|
||||
def mark_tracks_as_missing_locally(playlist_spotify_id):
|
||||
"""Removes specified tracks from the playlist's local DB table."""
|
||||
logger.info(f"Attempting to mark tracks as missing (delete locally) for playlist {playlist_spotify_id}.")
|
||||
try:
|
||||
track_ids = request.json
|
||||
if not isinstance(track_ids, list) or not all(isinstance(tid, str) for tid in track_ids):
|
||||
return jsonify({"error": "Invalid request body. Expecting a JSON array of track Spotify IDs."}), 400
|
||||
|
||||
if not get_watched_playlist(playlist_spotify_id):
|
||||
return jsonify({"error": f"Playlist {playlist_spotify_id} is not being watched."}), 404
|
||||
|
||||
deleted_count = remove_specific_tracks_from_playlist_table(playlist_spotify_id, track_ids)
|
||||
logger.info(f"Successfully removed {deleted_count} tracks locally for playlist {playlist_spotify_id}.")
|
||||
return jsonify({"message": f"Successfully removed {deleted_count} tracks locally for playlist {playlist_spotify_id}."}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking tracks as missing (deleting locally) for playlist {playlist_spotify_id}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Could not mark tracks as missing: {str(e)}"}), 500
|
||||
|
||||
@playlist_bp.route('/watch/list', methods=['GET'])
|
||||
def list_watched_playlists_endpoint():
|
||||
"""Lists all playlists currently in the watchlist."""
|
||||
try:
|
||||
playlists = get_watched_playlists()
|
||||
return jsonify(playlists), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing watched playlists: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Could not list watched playlists: {str(e)}"}), 500
|
||||
|
||||
@playlist_bp.route('/watch/trigger_check', methods=['POST'])
|
||||
def trigger_playlist_check_endpoint():
|
||||
"""Manually triggers the playlist checking mechanism for all watched playlists."""
|
||||
logger.info("Manual trigger for playlist check received for all playlists.")
|
||||
try:
|
||||
# Run check_watched_playlists without an ID to check all
|
||||
thread = threading.Thread(target=check_watched_playlists, args=(None,))
|
||||
thread.start()
|
||||
return jsonify({"message": "Playlist check triggered successfully in the background for all playlists."}), 202
|
||||
except Exception as e:
|
||||
logger.error(f"Error manually triggering playlist check for all: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Could not trigger playlist check for all: {str(e)}"}), 500
|
||||
|
||||
@playlist_bp.route('/watch/trigger_check/<string:playlist_spotify_id>', methods=['POST'])
|
||||
def trigger_specific_playlist_check_endpoint(playlist_spotify_id: str):
|
||||
"""Manually triggers the playlist checking mechanism for a specific playlist."""
|
||||
logger.info(f"Manual trigger for specific playlist check received for ID: {playlist_spotify_id}")
|
||||
try:
|
||||
# Check if the playlist is actually in the watchlist first
|
||||
watched_playlist = get_watched_playlist(playlist_spotify_id)
|
||||
if not watched_playlist:
|
||||
logger.warning(f"Trigger specific check: Playlist ID {playlist_spotify_id} not found in watchlist.")
|
||||
return jsonify({"error": f"Playlist {playlist_spotify_id} is not in the watchlist. Add it first."}), 404
|
||||
|
||||
# Run check_watched_playlists with the specific ID
|
||||
thread = threading.Thread(target=check_watched_playlists, args=(playlist_spotify_id,))
|
||||
thread.start()
|
||||
logger.info(f"Playlist check triggered in background for specific playlist ID: {playlist_spotify_id}")
|
||||
return jsonify({"message": f"Playlist check triggered successfully in the background for {playlist_spotify_id}."}), 202
|
||||
except Exception as e:
|
||||
logger.error(f"Error manually triggering specific playlist check for {playlist_spotify_id}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Could not trigger playlist check for {playlist_spotify_id}: {str(e)}"}), 500
|
||||
|
||||
Reference in New Issue
Block a user