Dateien nach "/" hochladen
This commit is contained in:
parent
341c3bbebd
commit
1c1b8663b4
658
app.py
Normal file
658
app.py
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, flash
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, login_user, logout_user, login_required, current_user, UserMixin
|
||||
import requests
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import subprocess
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.request import Request, urlopen
|
||||
import json
|
||||
import re
|
||||
from base64 import b64decode
|
||||
from seleniumbase import SB
|
||||
import psutil
|
||||
from zipfile import ZipFile
|
||||
from io import BytesIO
|
||||
from celery import Celery
|
||||
from celery.result import AsyncResult
|
||||
import redis
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get('SECRET_KEY', 'supergeheim')
|
||||
|
||||
# Database Configuration
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///aniworld_downloader.db')
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Celery Configuration
|
||||
app.config['CELERY'] = {
|
||||
'broker_url': os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0'),
|
||||
'result_backend': os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0'),
|
||||
'task_ignore_result': False,
|
||||
'task_serializer': 'json',
|
||||
'result_serializer': 'json',
|
||||
'accept_content': ['json'],
|
||||
'timezone': 'UTC',
|
||||
'enable_utc': True,
|
||||
'task_soft_time_limit': 3600, # 1 hour
|
||||
'task_time_limit': 3900, # 1 hour 5 minutes
|
||||
'worker_prefetch_multiplier': 1,
|
||||
'task_acks_late': True,
|
||||
'worker_max_tasks_per_child': 50,
|
||||
}
|
||||
|
||||
# Jellyfin Configuration
|
||||
JELLYFIN_URL = os.environ.get('JELLYFIN_URL', 'http://dein-jellyfin-server:8096')
|
||||
JELLYFIN_API_KEY = os.environ.get('JELLYFIN_API_KEY')
|
||||
|
||||
# VPN Configuration
|
||||
VPN_CONFIG_PATH = os.environ.get('VPN_CONFIG_PATH', '/etc/openvpn/config.ovpn')
|
||||
VPN_PID_FILE = "/tmp/aniworld_vpn.pid"
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = 'login'
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Celery Setup
|
||||
def make_celery(app):
|
||||
celery = Celery(
|
||||
app.import_name,
|
||||
backend=app.config['CELERY']['result_backend'],
|
||||
broker=app.config['CELERY']['broker_url']
|
||||
)
|
||||
celery.conf.update(app.config['CELERY'])
|
||||
|
||||
class ContextTask(celery.Task):
|
||||
def __call__(self, *args, **kwargs):
|
||||
with app.app_context():
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
celery.Task = ContextTask
|
||||
return celery
|
||||
|
||||
|
||||
celery = make_celery(app)
|
||||
|
||||
# Provider and language configurations
|
||||
provider_priority = ["Vidmoly", "VOE", "SpeedFiles", "Vidoza", "Doodstream", "Streamtape"]
|
||||
languages = ["German", "Ger-Sub", "Eng-Sub", "English"]
|
||||
|
||||
# Compilation patterns for content extraction
|
||||
VOE_PATTERNS = [re.compile(r"'hls': '(?P<url>.+)'"),
|
||||
re.compile(r'prompt\("Node",\s*"(?P<url>[^"]+)"'),
|
||||
re.compile(r"window\.location\.href = '(?P<url>[^']+)'")]
|
||||
STREAMTAPE_PATTERN = re.compile(r'get_video\?id=[^&\'\s]+&expires=[^&\'\s]+&ip=[^&\'\s]+&token=[^&\'\s]+\'')
|
||||
DOODSTREAM_PATTERN = re.compile(r"/pass_md5/[\w-]+/(?P<token>[\w-]+)")
|
||||
VIDMOLY_PATTERN = re.compile(r"sources: \[{file:\"(?P<url>.*?)\"}]")
|
||||
SPEEDFILES_PATTERN = re.compile(r"var _0x5opu234 = \"(?P<content>.*?)\";")
|
||||
|
||||
|
||||
# Database Models
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
jellyfin_username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
jellyfin_user_id = db.Column(db.String(100), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
downloads = db.relationship('Download', backref='user', lazy=True)
|
||||
|
||||
|
||||
class Download(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
season = db.Column(db.Integer)
|
||||
episode = db.Column(db.Integer)
|
||||
provider = db.Column(db.String(50), nullable=False)
|
||||
language = db.Column(db.String(20), nullable=False)
|
||||
status = db.Column(db.String(20), default='queued') # queued, downloading, completed, failed, cancelled
|
||||
progress = db.Column(db.Float, default=0.0)
|
||||
file_path = db.Column(db.String(500))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
download_url = db.Column(db.String(1000))
|
||||
redirect_link = db.Column(db.String(1000))
|
||||
celery_task_id = db.Column(db.String(100)) # Celery Task ID
|
||||
error_message = db.Column(db.Text)
|
||||
|
||||
|
||||
class ProviderError(Exception):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class LanguageError(Exception):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
|
||||
# Jellyfin Authentication Functions
|
||||
def authenticate_jellyfin(username, password):
|
||||
"""Authenticate user with Jellyfin server"""
|
||||
try:
|
||||
auth_url = f"{JELLYFIN_URL}/Users/authenticatebyname"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Emby-Authorization': f'MediaBrowser Client="AniWorld Downloader", Device="WebApp", DeviceId="webapp-{random.randint(1000, 9999)}", Version="1.0.0"'
|
||||
}
|
||||
|
||||
payload = {
|
||||
"Username": username,
|
||||
"Pw": password
|
||||
}
|
||||
|
||||
response = requests.post(auth_url, json=payload, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return {
|
||||
'user_id': data['User']['Id'],
|
||||
'username': data['User']['Name'],
|
||||
'access_token': data['AccessToken']
|
||||
}
|
||||
else:
|
||||
logger.error(f"Jellyfin authentication failed: {response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during Jellyfin authentication: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def verify_jellyfin_token(access_token):
|
||||
"""Verify if Jellyfin token is still valid"""
|
||||
try:
|
||||
headers = {
|
||||
'X-Emby-Token': access_token
|
||||
}
|
||||
response = requests.get(f"{JELLYFIN_URL}/System/Info", headers=headers)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying Jellyfin token: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# VPN Functions (Verbessert)
|
||||
def get_vpn_process():
|
||||
"""Find the specific VPN process started by this script"""
|
||||
try:
|
||||
if os.path.exists(VPN_PID_FILE):
|
||||
with open(VPN_PID_FILE, 'r') as f:
|
||||
pid = int(f.read().strip())
|
||||
|
||||
if psutil.pid_exists(pid):
|
||||
proc = psutil.Process(pid)
|
||||
if 'openvpn' in proc.name().lower():
|
||||
return proc
|
||||
|
||||
# Fallback: Look for openvpn process with our config file
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||
try:
|
||||
if ('openvpn' in proc.info['name'].lower() and
|
||||
any(VPN_CONFIG_PATH in cmd for cmd in proc.info['cmdline'])):
|
||||
return proc
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding VPN process: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def disconnect_vpn():
|
||||
"""Disconnect only our specific VPN connection"""
|
||||
try:
|
||||
vpn_proc = get_vpn_process()
|
||||
if vpn_proc:
|
||||
logger.info(f"Terminating VPN process with PID: {vpn_proc.pid}")
|
||||
vpn_proc.terminate()
|
||||
|
||||
try:
|
||||
vpn_proc.wait(timeout=5)
|
||||
except psutil.TimeoutExpired:
|
||||
logger.warning("VPN process didn't terminate gracefully, forcing kill")
|
||||
vpn_proc.kill()
|
||||
|
||||
if os.path.exists(VPN_PID_FILE):
|
||||
os.remove(VPN_PID_FILE)
|
||||
|
||||
logger.info("VPN disconnected successfully")
|
||||
return True
|
||||
else:
|
||||
logger.info("No VPN process found to disconnect")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error disconnecting VPN: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def connect_vpn():
|
||||
"""Connect to VPN and save PID"""
|
||||
try:
|
||||
proc = subprocess.Popen([
|
||||
'sudo', 'openvpn',
|
||||
'--config', VPN_CONFIG_PATH,
|
||||
'--daemon',
|
||||
'--writepid', VPN_PID_FILE
|
||||
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
if os.path.exists(VPN_PID_FILE):
|
||||
with open(VPN_PID_FILE, 'r') as f:
|
||||
pid = int(f.read().strip())
|
||||
|
||||
if psutil.pid_exists(pid):
|
||||
logger.info(f"VPN connected successfully with PID: {pid}")
|
||||
return True
|
||||
|
||||
logger.error("VPN connection failed - PID file not created")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting VPN: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def reconnect_vpn():
|
||||
"""Reconnect to VPN without affecting other VPN connections"""
|
||||
try:
|
||||
logger.info("Starting VPN reconnection...")
|
||||
|
||||
if not disconnect_vpn():
|
||||
logger.error("Failed to disconnect VPN")
|
||||
return False
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
if connect_vpn():
|
||||
time.sleep(5)
|
||||
logger.info("VPN reconnected successfully")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to reconnect VPN")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reconnecting VPN: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Content extraction functions (keeping existing ones)
|
||||
def get_titles_dict(url):
|
||||
"""Get titles dictionary from streaming sites"""
|
||||
try:
|
||||
html_response = urlopen(url)
|
||||
soup = BeautifulSoup(html_response, "html.parser")
|
||||
matching_li_elements = str(soup.find_all("li")).split("</a></li>, <li><a ")
|
||||
index_list = []
|
||||
for i in matching_li_elements:
|
||||
if "data-alternative-title" not in i:
|
||||
index_list.append(matching_li_elements.index(i))
|
||||
index_list = sorted(index_list, reverse=True)
|
||||
for i in index_list:
|
||||
matching_li_elements.pop(i)
|
||||
title_link_dict = {}
|
||||
for i in range(len(matching_li_elements)):
|
||||
matching_li_elements[i] = matching_li_elements[i][24:]
|
||||
for i in matching_li_elements:
|
||||
title_link_dict.update({i[i.index('" title') + 9:i.index(" Stream anschauen")] + "," + i[:i.index('"')]: i[
|
||||
i.index(
|
||||
'href="') + 20: i.index(
|
||||
'" title')]})
|
||||
return title_link_dict
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting titles from {url}: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
# [Alle anderen Content-Extraction-Funktionen bleiben gleich...]
|
||||
# (get_titles_dict_2, restructure_dict, extract_lang_key_mapping, etc.)
|
||||
|
||||
# CELERY TASKS (Getrennte Download-Logik)
|
||||
@celery.task(bind=True)
|
||||
def download_episode_task(self, download_id):
|
||||
"""Celery task for downloading episodes"""
|
||||
try:
|
||||
# Update task ID in database
|
||||
download = Download.query.get(download_id)
|
||||
if not download:
|
||||
logger.error(f"Download {download_id} not found")
|
||||
return {'status': 'failed', 'error': 'Download not found'}
|
||||
|
||||
download.celery_task_id = self.request.id
|
||||
download.status = 'downloading'
|
||||
db.session.commit()
|
||||
|
||||
# Get or find content URL
|
||||
if not download.download_url:
|
||||
if not download.redirect_link:
|
||||
error_msg = f"No redirect link for download {download_id}"
|
||||
logger.error(error_msg)
|
||||
download.status = 'failed'
|
||||
download.error_message = error_msg
|
||||
db.session.commit()
|
||||
return {'status': 'failed', 'error': error_msg}
|
||||
|
||||
content_url = find_content_url(download.redirect_link, download.provider)
|
||||
if not content_url:
|
||||
error_msg = f"Could not find content URL for {download.redirect_link}"
|
||||
logger.error(error_msg)
|
||||
download.status = 'failed'
|
||||
download.error_message = error_msg
|
||||
db.session.commit()
|
||||
return {'status': 'failed', 'error': error_msg}
|
||||
|
||||
download.download_url = content_url
|
||||
db.session.commit()
|
||||
|
||||
# Create file path
|
||||
base_dir = os.path.join(app.root_path, "downloads", str(download.user_id), download.title)
|
||||
if download.season:
|
||||
file_path = os.path.join(base_dir, f"Season {download.season}",
|
||||
f"S{download.season}-E{download.episode}-{download.title}.mp4")
|
||||
else:
|
||||
file_path = os.path.join(base_dir, "Movies", f"Movie {download.episode}-{download.title}.mp4")
|
||||
|
||||
# Create directory structure
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
# Download with ffmpeg
|
||||
ffmpeg_cmd = ["ffmpeg", "-i", download.download_url, "-c", "copy", "-nostdin", file_path]
|
||||
|
||||
if download.provider == "Doodstream":
|
||||
ffmpeg_cmd.insert(1, "Referer: https://d0000d.com/")
|
||||
ffmpeg_cmd.insert(1, "-headers")
|
||||
elif download.provider == "Vidmoly":
|
||||
ffmpeg_cmd.insert(1, "Referer: https://vidmoly.to/")
|
||||
ffmpeg_cmd.insert(1, "-headers")
|
||||
|
||||
process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
# Monitor download progress
|
||||
start_time = time.time()
|
||||
while process.poll() is None:
|
||||
# Update progress
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > 0:
|
||||
progress = min(95.0, (elapsed / 300) * 100) # Estimate 5 minutes
|
||||
download.progress = progress
|
||||
db.session.commit()
|
||||
|
||||
# Update Celery task progress
|
||||
self.update_state(
|
||||
state='PROGRESS',
|
||||
meta={'current': progress, 'total': 100, 'status': f'Downloading {download.title}...'}
|
||||
)
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
# Check if download completed successfully
|
||||
if process.returncode == 0 and os.path.exists(file_path):
|
||||
download.status = 'completed'
|
||||
download.progress = 100.0
|
||||
download.file_path = file_path
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Download completed: {file_path}")
|
||||
|
||||
# Reconnect VPN after download
|
||||
reconnect_vpn()
|
||||
|
||||
return {'status': 'completed', 'file_path': file_path}
|
||||
else:
|
||||
error_msg = f"Download failed: {file_path}"
|
||||
download.status = 'failed'
|
||||
download.error_message = error_msg
|
||||
db.session.commit()
|
||||
|
||||
logger.error(error_msg)
|
||||
|
||||
# Clean up partial file
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
return {'status': 'failed', 'error': error_msg}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error downloading episode {download_id}: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
|
||||
download = Download.query.get(download_id)
|
||||
if download:
|
||||
download.status = 'failed'
|
||||
download.error_message = error_msg
|
||||
db.session.commit()
|
||||
|
||||
return {'status': 'failed', 'error': error_msg}
|
||||
|
||||
|
||||
# [Alle anderen Hilfsfunktionen bleiben gleich...]
|
||||
# (find_content_url, get_season_count, get_episodes, etc.)
|
||||
|
||||
# Routes
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
if request.is_json:
|
||||
data = request.get_json()
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
else:
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
|
||||
jellyfin_user = authenticate_jellyfin(username, password)
|
||||
if jellyfin_user:
|
||||
user = User.query.filter_by(jellyfin_username=username).first()
|
||||
if not user:
|
||||
user = User(
|
||||
jellyfin_username=username,
|
||||
jellyfin_user_id=jellyfin_user['user_id']
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
login_user(user)
|
||||
if request.is_json:
|
||||
return jsonify({'success': True})
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
if request.is_json:
|
||||
return jsonify({'success': False, 'error': 'Login fehlgeschlagen'}), 401
|
||||
flash('Ungültige Jellyfin-Anmeldedaten')
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
@app.route('/api/download/start', methods=['POST'])
|
||||
@login_required
|
||||
def start_download():
|
||||
data = request.json
|
||||
link = data.get('link')
|
||||
provider = data.get('provider', 'Vidmoly')
|
||||
language = data.get('language', 'German')
|
||||
content_type = data.get('content_type', 'episodes')
|
||||
|
||||
if not link:
|
||||
return jsonify({'error': 'No link provided'}), 400
|
||||
|
||||
normalized_link = validate_link(link)
|
||||
if not normalized_link:
|
||||
return jsonify({'error': 'Invalid link format'}), 400
|
||||
|
||||
title = extract_title(normalized_link)
|
||||
site = normalized_link[:normalized_link.index(".to/") + 3]
|
||||
|
||||
download_ids = []
|
||||
task_ids = []
|
||||
|
||||
if content_type == 'episodes':
|
||||
season_start = data.get('season_start', 1)
|
||||
season_end = data.get('season_end', 1)
|
||||
episode_start = data.get('episode_start', 1)
|
||||
episode_end = data.get('episode_end', 1)
|
||||
|
||||
for season in range(season_start, season_end + 1):
|
||||
ep_start = episode_start if season == season_start else 1
|
||||
ep_end = episode_end if season == season_end else get_episodes(normalized_link, season)
|
||||
|
||||
for episode in range(ep_start, ep_end + 1):
|
||||
if "bs.to" in normalized_link:
|
||||
episode_link = normalized_link + "{}/{}-".format(season, episode)
|
||||
else:
|
||||
episode_link = normalized_link + "staffel-{}/episode-{}".format(season, episode)
|
||||
|
||||
try:
|
||||
redirect_link = get_redirect_link_by_provider(
|
||||
site, episode_link, language, provider, season, episode, current_user.id
|
||||
)
|
||||
|
||||
if redirect_link:
|
||||
download = Download(
|
||||
title=title,
|
||||
season=season,
|
||||
episode=episode,
|
||||
provider=provider,
|
||||
language=language,
|
||||
redirect_link=redirect_link,
|
||||
user_id=current_user.id
|
||||
)
|
||||
db.session.add(download)
|
||||
db.session.commit()
|
||||
download_ids.append(download.id)
|
||||
|
||||
# Start Celery task
|
||||
task = download_episode_task.delay(download.id)
|
||||
task_ids.append(task.id)
|
||||
|
||||
# Update download with task ID
|
||||
download.celery_task_id = task.id
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding download for S{season}E{episode}: {e}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'downloads_added': len(download_ids),
|
||||
'download_ids': download_ids,
|
||||
'task_ids': task_ids
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/download/<int:download_id>/status')
|
||||
@login_required
|
||||
def download_status(download_id):
|
||||
download = Download.query.get_or_404(download_id)
|
||||
if download.user_id != current_user.id:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
result_data = {
|
||||
'id': download.id,
|
||||
'status': download.status,
|
||||
'progress': download.progress,
|
||||
'error_message': download.error_message
|
||||
}
|
||||
|
||||
# Get Celery task status if available
|
||||
if download.celery_task_id:
|
||||
try:
|
||||
task_result = AsyncResult(download.celery_task_id, app=celery)
|
||||
if task_result.state == 'PROGRESS':
|
||||
result_data['celery_progress'] = task_result.info
|
||||
elif task_result.state == 'SUCCESS':
|
||||
result_data['celery_result'] = task_result.result
|
||||
elif task_result.state == 'FAILURE':
|
||||
result_data['celery_error'] = str(task_result.info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Celery task status: {e}")
|
||||
|
||||
return jsonify(result_data)
|
||||
|
||||
|
||||
@app.route('/api/download/<int:download_id>/cancel', methods=['POST'])
|
||||
@login_required
|
||||
def cancel_download(download_id):
|
||||
download = Download.query.get_or_404(download_id)
|
||||
if download.user_id != current_user.id:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
if download.status in ['queued', 'downloading']:
|
||||
download.status = 'cancelled'
|
||||
db.session.commit()
|
||||
|
||||
# Cancel Celery task if exists
|
||||
if download.celery_task_id:
|
||||
try:
|
||||
celery.control.revoke(download.celery_task_id, terminate=True)
|
||||
logger.info(f"Cancelled Celery task {download.celery_task_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error cancelling Celery task: {e}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'status': download.status
|
||||
})
|
||||
|
||||
|
||||
@app.route('/queue')
|
||||
@login_required
|
||||
def queue():
|
||||
downloads = Download.query.filter_by(user_id=current_user.id).order_by(Download.created_at.desc()).all()
|
||||
return render_template('queue.html', downloads=downloads)
|
||||
|
||||
|
||||
@app.route('/api/downloads')
|
||||
@login_required
|
||||
def api_downloads():
|
||||
downloads = Download.query.filter_by(user_id=current_user.id).all()
|
||||
return jsonify([{
|
||||
'id': d.id,
|
||||
'title': d.title,
|
||||
'season': d.season,
|
||||
'episode': d.episode,
|
||||
'status': d.status,
|
||||
'progress': d.progress,
|
||||
'provider': d.provider,
|
||||
'language': d.language,
|
||||
'created_at': d.created_at.isoformat(),
|
||||
'error_message': d.error_message
|
||||
} for d in downloads])
|
||||
|
||||
|
||||
# [Alle anderen Routes bleiben gleich...]
|
||||
|
||||
if __name__ == '__main__':
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
app.run(debug=False, host='0.0.0.0', port=5000) # debug=False für Production!
|
||||
Loading…
Reference in New Issue
Block a user