543 lines
17 KiB
Python
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)
|