2026-03-31 17:39:27 -05:00

543 lines
17 KiB
Python

#!/usr/bin/env python3
"""
Flask API Server for YouTube Web Interface
Provides REST API endpoints for searching, downloading, and managing YouTube content
"""
import json
import os
import sys
import uuid
from datetime import datetime
from pathlib import Path
from flask import Flask, jsonify, request
from flask_cors import CORS
# Add parent directory to path to import YouTubeCLI
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from youtube_cli.main import YouTubeCLI
# Initialize Flask app
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "*"}})
# Create YouTubeCLI instance
yt_cli = YouTubeCLI()
# ==================== Utility Functions ====================
def get_config():
"""Get current configuration from YouTubeCLI instance."""
return yt_cli.config
def make_response(data, status=200):
"""Create a JSON response with proper structure."""
return jsonify({
"success": True,
"data": data
}), status
def make_error_response(message, status=400):
"""Create an error response."""
return jsonify({
"success": False,
"error": message
}), status
# ==================== Health & Config ====================
@app.route('/api/health', methods=['GET'])
def health_check():
"""Health check endpoint."""
try:
yt_dlp_version = yt_cli.get_yt_dlp_version()
return make_response({
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"yt_dlp_version": yt_dlp_version
})
except Exception as e:
return make_error_response(f"Health check failed: {str(e)}", 500)
@app.route('/api/config', methods=['GET'])
def get_config_endpoint():
"""Get current configuration."""
try:
config = get_config()
return make_response({
"download_dir": config.get("download_dir"),
"default_locations": config.get("default_locations", []),
"max_videos_per_page": config.get("max_videos_per_page", 15),
"network_share_path": config.get("network_share_path"),
"default_network_subfolder": config.get("default_network_subfolder")
})
except Exception as e:
return make_error_response(f"Failed to get config: {str(e)}", 500)
@app.route('/api/categories', methods=['GET'])
def get_categories():
"""Get available download categories."""
try:
config = get_config()
categories = yt_cli.get_categories(config)
return make_response({
"categories": categories,
"default_download_dir": config.get("download_dir")
})
except Exception as e:
return make_error_response(f"Failed to get categories: {str(e)}", 500)
# ==================== Search ====================
@app.route('/api/search', methods=['GET'])
def search():
"""Search for videos."""
try:
query = request.args.get('q', '').strip()
page = int(request.args.get('page', 1))
if not query:
return make_error_response("Search query is required", 400)
if page < 1:
return make_error_response("Page must be greater than 0", 400)
config = get_config()
videos = yt_cli.search_videos(query, config, page=page, return_results=True)
return make_response({
"query": query,
"page": page,
"videos": videos
})
except Exception as e:
return make_error_response(f"Search failed: {str(e)}", 500)
# ==================== Download ====================
# Queue to track downloads
download_queue = {}
@app.route('/api/download', methods=['POST'])
def download_video():
"""Download a video."""
try:
data = request.get_json()
url = data.get('url', '').strip() if data else ''
category = data.get('category') if data else None
network_folder = data.get('network_folder') if data else None
if not url:
return make_error_response("Video URL is required", 400)
config = get_config()
# Generate queue ID
queue_id = str(uuid.uuid4())
# Create queue item
queue_item = {
"id": queue_id,
"url": url,
"category": category,
"network_folder": network_folder,
"status": "pending",
"created_at": datetime.utcnow().isoformat(),
"progress": 0,
"message": "Queued for download"
}
# Add to queue
download_queue[queue_id] = queue_item
# Start download in background (simplified - would need threading in production)
# For now, we do it synchronously
def process_download():
try:
download_queue[queue_id]["status"] = "downloading"
download_queue[queue_id]["message"] = "Starting download..."
# Download the video
success = yt_cli.download_video(
url=url,
config=config,
network_folder=network_folder,
category=category
)
if success:
download_queue[queue_id]["status"] = "completed"
download_queue[queue_id]["message"] = "Download completed successfully"
download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat()
else:
download_queue[queue_id]["status"] = "failed"
download_queue[queue_id]["message"] = "Download failed"
download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat()
except Exception as e:
download_queue[queue_id]["status"] = "failed"
download_queue[queue_id]["message"] = str(e)
download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat()
# Process download (synchronous for now)
process_download()
return make_response({
"queue_id": queue_id,
"status": download_queue[queue_id]["status"],
"message": download_queue[queue_id]["message"]
})
except Exception as e:
return make_error_response(f"Download failed: {str(e)}", 500)
@app.route('/api/download/playlist', methods=['POST'])
def download_playlist():
"""Download a playlist."""
try:
data = request.get_json()
url = data.get('url', '').strip() if data else ''
category = data.get('category') if data else None
network_folder = data.get('network_folder') if data else None
if not url:
return make_error_response("Playlist URL is required", 400)
config = get_config()
# Generate queue ID
queue_id = str(uuid.uuid4())
# Create queue item
queue_item = {
"id": queue_id,
"url": url,
"category": category,
"network_folder": network_folder,
"type": "playlist",
"status": "pending",
"created_at": datetime.utcnow().isoformat(),
"progress": 0,
"message": "Queued for download",
"videos": []
}
# Add to queue
download_queue[queue_id] = queue_item
# Start playlist download
def process_playlist_download():
try:
download_queue[queue_id]["status"] = "downloading"
download_queue[queue_id]["message"] = "Starting playlist download..."
# Download the playlist
result = yt_cli.download_playlist(
url=url,
config=config,
network_folder=network_folder,
category=category
)
if result:
download_queue[queue_id]["status"] = "completed"
download_queue[queue_id]["message"] = "Playlist download completed successfully"
download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat()
download_queue[queue_id]["videos"] = result
else:
download_queue[queue_id]["status"] = "failed"
download_queue[queue_id]["message"] = "Playlist download failed"
download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat()
except Exception as e:
download_queue[queue_id]["status"] = "failed"
download_queue[queue_id]["message"] = str(e)
download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat()
# Process playlist download (synchronous for now)
process_playlist_download()
return make_response({
"queue_id": queue_id,
"status": download_queue[queue_id]["status"],
"message": download_queue[queue_id]["message"],
"videos": download_queue[queue_id].get("videos", [])
})
except Exception as e:
return make_error_response(f"Playlist download failed: {str(e)}", 500)
# ==================== Archive ====================
@app.route('/api/archive', methods=['GET'])
def get_archive():
"""Get download archive."""
try:
archive = yt_cli.downloaded_videos
return make_response({
"archive": archive,
"total": len(archive)
})
except Exception as e:
return make_error_response(f"Failed to get archive: {str(e)}", 500)
@app.route('/api/archive/<video_id>', methods=['DELETE'])
def remove_from_archive(video_id):
"""Remove a video from the archive."""
try:
# Remove from archive
if video_id in yt_cli.downloaded_videos:
del yt_cli.downloaded_videos[video_id]
yt_cli.save_archive()
return make_response({"message": f"Video {video_id} removed from archive"})
else:
return make_error_response(f"Video {video_id} not found in archive", 404)
except Exception as e:
return make_error_response(f"Failed to remove from archive: {str(e)}", 500)
# ==================== Queue ====================
@app.route('/api/queue', methods=['GET'])
def get_queue():
"""Get all queue items."""
try:
return make_response({
"queue": list(download_queue.values()),
"total": len(download_queue)
})
except Exception as e:
return make_error_response(f"Failed to get queue: {str(e)}", 500)
@app.route('/api/queue/<queue_id>', methods=['DELETE'])
def remove_from_queue(queue_id):
"""Remove an item from the queue."""
try:
if queue_id in download_queue:
del download_queue[queue_id]
return make_response({"message": f"Item {queue_id} removed from queue"})
else:
return make_error_response(f"Queue item {queue_id} not found", 404)
except Exception as e:
return make_error_response(f"Failed to remove from queue: {str(e)}", 500)
@app.route('/api/queue/<queue_id>/retry', methods=['POST'])
def retry_download(queue_id):
"""Retry a failed download."""
try:
if queue_id not in download_queue:
return make_error_response(f"Queue item {queue_id} not found", 404)
item = download_queue[queue_id]
# Reset queue item status
item["status"] = "pending"
item["message"] = "Retry queued"
item["progress"] = 0
# Re-download based on type
if item.get("type") == "playlist":
return download_playlist_wrapper(queue_id)
else:
return download_video_wrapper(queue_id)
except Exception as e:
return make_error_response(f"Retry failed: {str(e)}", 500)
@app.route('/api/queue/<queue_id>/cancel', methods=['POST'])
def cancel_download(queue_id):
"""Cancel a download."""
try:
if queue_id not in download_queue:
return make_error_response(f"Queue item {queue_id} not found", 404)
item = download_queue[queue_id]
if item["status"] in ["completed", "failed"]:
return make_error_response(f"Cannot cancel {item['status']} download", 400)
item["status"] = "cancelled"
item["message"] = "Download cancelled by user"
return make_response({
"queue_id": queue_id,
"status": item["status"],
"message": item["message"]
})
except Exception as e:
return make_error_response(f"Failed to cancel download: {str(e)}", 500)
@app.route('/api/queue/<queue_id>/status', methods=['GET'])
def get_queue_status(queue_id):
"""Get the status of a queue item."""
try:
if queue_id not in download_queue:
return make_error_response(f"Queue item {queue_id} not found", 404)
return make_response({
"queue_id": queue_id,
"status": download_queue[queue_id]["status"]
})
except Exception as e:
return make_error_response(f"Failed to get queue status: {str(e)}", 500)
@app.route('/api/queue/clear/completed', methods=['POST'])
def clear_completed():
"""Clear completed items from the queue."""
try:
completed_ids = [
queue_id for queue_id, item in download_queue.items()
if item["status"] == "completed"
]
for queue_id in completed_ids:
del download_queue[queue_id]
return make_response({
"cleared": completed_ids,
"count": len(completed_ids)
})
except Exception as e:
return make_error_response(f"Failed to clear completed items: {str(e)}", 500)
@app.route('/api/queue/clear/failed', methods=['POST'])
def clear_failed():
"""Clear failed items from the queue."""
try:
failed_ids = [
queue_id for queue_id, item in download_queue.items()
if item["status"] == "failed"
]
for queue_id in failed_ids:
del download_queue[queue_id]
return make_response({
"cleared": failed_ids,
"count": len(failed_ids)
})
except Exception as e:
return make_error_response(f"Failed to clear failed items: {str(e)}", 500)
# ==================== Helper Functions ====================
def download_video_wrapper(queue_id):
"""Wrapper to re-download a video from queue."""
try:
item = download_queue[queue_id]
config = get_config()
item["status"] = "downloading"
item["message"] = "Starting download..."
success = yt_cli.download_video(
url=item["url"],
config=config,
network_folder=item.get("network_folder"),
category=item.get("category")
)
if success:
item["status"] = "completed"
item["message"] = "Download completed successfully"
item["completed_at"] = datetime.utcnow().isoformat()
else:
item["status"] = "failed"
item["message"] = "Download failed"
item["completed_at"] = datetime.utcnow().isoformat()
return make_response({
"queue_id": queue_id,
"status": item["status"],
"message": item["message"]
})
except Exception as e:
download_queue[queue_id]["status"] = "failed"
download_queue[queue_id]["message"] = str(e)
return make_error_response(f"Download failed: {str(e)}", 500)
def download_playlist_wrapper(queue_id):
"""Wrapper to re-download a playlist from queue."""
try:
item = download_queue[queue_id]
config = get_config()
item["status"] = "downloading"
item["message"] = "Starting playlist download..."
result = yt_cli.download_playlist(
url=item["url"],
config=config,
network_folder=item.get("network_folder"),
category=item.get("category")
)
if result:
item["status"] = "completed"
item["message"] = "Playlist download completed successfully"
item["completed_at"] = datetime.utcnow().isoformat()
item["videos"] = result
else:
item["status"] = "failed"
item["message"] = "Playlist download failed"
item["completed_at"] = datetime.utcnow().isoformat()
return make_response({
"queue_id": queue_id,
"status": item["status"],
"message": item["message"],
"videos": item.get("videos", [])
})
except Exception as e:
download_queue[queue_id]["status"] = "failed"
download_queue[queue_id]["message"] = str(e)
return make_error_response(f"Playlist download failed: {str(e)}", 500)
# ==================== Error Handlers ====================
@app.errorhandler(404)
def not_found(error):
"""Handle 404 errors."""
return make_error_response("Endpoint not found", 404)
@app.errorhandler(500)
def internal_error(error):
"""Handle 500 errors."""
return make_error_response("Internal server error", 500)
# ==================== Run Server ====================
if __name__ == '__main__':
# Default to port 4096
port = int(os.environ.get('PORT', 4096))
debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
app.run(host='0.0.0.0', port=port, debug=debug)