This commit is contained in:
DengDai
2025-12-08 14:31:21 +08:00
commit ad2c65affb
35 changed files with 3500 additions and 0 deletions

1
routes/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Routes package initialization

64
routes/auth.py Normal file
View File

@@ -0,0 +1,64 @@
from flask import Blueprint, render_template, request, redirect, url_for, session, jsonify
import sqlite3
import hashlib
import os
auth_bp = Blueprint('auth', __name__)
def get_db_connection():
conn = sqlite3.connect('pt_manager.db')
conn.row_factory = sqlite3.Row
return conn
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
conn.close()
if user and user['password_hash'] == password: # In production, use proper password hashing
session['user_id'] = user['id']
session['username'] = user['username']
session['role'] = user['role']
return redirect(url_for('main.index'))
else:
return render_template('auth/login.html', error='Invalid credentials')
return render_template('auth/login.html')
@auth_bp.route('/logout')
def logout():
session.clear()
return redirect(url_for('auth.login'))
@auth_bp.route('/change_password', methods=['GET', 'POST'])
def change_password():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
if request.method == 'POST':
current_password = request.form['current_password']
new_password = request.form['new_password']
confirm_password = request.form['confirm_password']
if new_password != confirm_password:
return render_template('auth/change_password.html', error='New passwords do not match')
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE id = ?', (session['user_id'],)).fetchone()
if user and user['password_hash'] == current_password: # In production, use proper password hashing
conn.execute('UPDATE users SET password_hash = ? WHERE id = ?',
(new_password, session['user_id']))
conn.commit()
conn.close()
return redirect(url_for('main.index'))
else:
conn.close()
return render_template('auth/change_password.html', error='Current password is incorrect')
return render_template('auth/change_password.html')

15
routes/main.py Normal file
View File

@@ -0,0 +1,15 @@
from flask import Blueprint, render_template, session, redirect, url_for
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
return render_template('main/index.html')
@main_bp.route('/dashboard')
def dashboard():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
return render_template('main/dashboard.html')

121
routes/qbittorrent.py Normal file
View File

@@ -0,0 +1,121 @@
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
import sqlite3
import qbittorrentapi
import sys
import os
# Import format functions
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from utils.format import format_file_size, format_status
qbittorrent_bp = Blueprint('qbittorrent', __name__)
def get_db_connection():
conn = sqlite3.connect('pt_manager.db')
conn.row_factory = sqlite3.Row
return conn
def get_qbittorrent_client():
conn = get_db_connection()
client_config = conn.execute(
"SELECT * FROM clients WHERE name = 'qbittorrent' AND enabled = 1"
).fetchone()
conn.close()
if not client_config:
return None
try:
qb = qbittorrentapi.Client(
host=client_config['host'],
port=client_config['port'],
username=client_config['username'],
password=client_config['password']
)
qb.auth_log_in()
return qb
except Exception as e:
print(f"Failed to connect to qBittorrent: {e}")
return None
@qbittorrent_bp.route('/qbittorrent')
def qbittorrent_index():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
return render_template('qbittorrent/index.html')
@qbittorrent_bp.route('/qbittorrent/torrents')
def torrents():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
qb = get_qbittorrent_client()
if not qb:
return render_template('qbittorrent/torrents.html', error='qBittorrent client not configured or unavailable')
try:
torrents = qb.torrents_info()
# Process torrents to format file sizes and statuses
processed_torrents = []
for torrent in torrents:
processed_torrent = {
'hash': torrent.hash,
'name': torrent.name,
'size': format_file_size(torrent.size),
'progress': torrent.progress,
'state': torrent.state,
'status': format_status(torrent.state),
'num_seeds': getattr(torrent, 'num_seeds', 0),
'num_leechs': getattr(torrent, 'num_leechs', 0)
}
processed_torrents.append(processed_torrent)
return render_template('qbittorrent/torrents.html', torrents=processed_torrents)
except Exception as e:
return render_template('qbittorrent/torrents.html', error=f'Failed to fetch torrents: {str(e)}')
@qbittorrent_bp.route('/qbittorrent/torrent/<info_hash>/pause', methods=['POST'])
def pause_torrent(info_hash):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
qb = get_qbittorrent_client()
if not qb:
return jsonify({'error': 'qBittorrent client not configured or unavailable'}), 500
try:
qb.torrents_pause(hashes=info_hash)
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@qbittorrent_bp.route('/qbittorrent/torrent/<info_hash>/resume', methods=['POST'])
def resume_torrent(info_hash):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
qb = get_qbittorrent_client()
if not qb:
return jsonify({'error': 'qBittorrent client not configured or unavailable'}), 500
try:
qb.torrents_resume(hashes=info_hash)
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@qbittorrent_bp.route('/qbittorrent/torrent/<info_hash>/delete', methods=['POST'])
def delete_torrent(info_hash):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
qb = get_qbittorrent_client()
if not qb:
return jsonify({'error': 'qBittorrent client not configured or unavailable'}), 500
try:
# Delete torrent and data
qb.torrents_delete(hashes=info_hash, delete_files=True)
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500

162
routes/settings.py Normal file
View File

@@ -0,0 +1,162 @@
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
import sqlite3
settings_bp = Blueprint('settings', __name__)
def get_db_connection():
conn = sqlite3.connect('pt_manager.db')
conn.row_factory = sqlite3.Row
return conn
@settings_bp.route('/settings')
def settings_index():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
if session['role'] != 'admin':
return redirect(url_for('main.index'))
conn = get_db_connection()
# Get NexusPHP settings
nexusphp_site_url = conn.execute(
"SELECT value FROM settings WHERE key = 'nexusphp_site_url'"
).fetchone()
nexusphp_api_token = conn.execute(
"SELECT value FROM settings WHERE key = 'nexusphp_api_token'"
).fetchone()
# Get client settings
qbittorrent_config = conn.execute(
"SELECT * FROM clients WHERE name = 'qbittorrent'"
).fetchone()
transmission_config = conn.execute(
"SELECT * FROM clients WHERE name = 'transmission'"
).fetchone()
conn.close()
return render_template('settings/index.html',
nexusphp_site_url=nexusphp_site_url['value'] if nexusphp_site_url else '',
nexusphp_api_token=nexusphp_api_token['value'] if nexusphp_api_token else '',
qbittorrent_config=qbittorrent_config,
transmission_config=transmission_config)
@settings_bp.route('/settings/nexusphp', methods=['POST'])
def save_nexusphp_settings():
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
if session['role'] != 'admin':
return jsonify({'error': 'Admin access required'}), 403
site_url = request.form.get('site_url')
api_token = request.form.get('api_token')
conn = get_db_connection()
try:
# Save or update site URL
conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
('nexusphp_site_url', site_url)
)
# Save or update API token
conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
('nexusphp_api_token', api_token)
)
print(api_token)#262|wEGbxaqybJ6ZfLZAAtxX0oiQTymFcelHpc6YHims27a70898
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 500
@settings_bp.route('/settings/qbittorrent', methods=['POST'])
def save_qbittorrent_settings():
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
if session['role'] != 'admin':
return jsonify({'error': 'Admin access required'}), 403
host = request.form.get('host')
port = request.form.get('port')
username = request.form.get('username')
password = request.form.get('password')
enabled = bool(request.form.get('enabled'))
conn = get_db_connection()
try:
# Check if config exists
existing = conn.execute(
"SELECT id FROM clients WHERE name = 'qbittorrent'"
).fetchone()
if existing:
# Update existing config
conn.execute(
"""UPDATE clients SET host = ?, port = ?, username = ?, password = ?, enabled = ?
WHERE name = 'qbittorrent'""",
(host, port, username, password, enabled)
)
else:
# Insert new config
conn.execute(
"""INSERT INTO clients (name, host, port, username, password, enabled)
VALUES (?, ?, ?, ?, ?, ?)""",
('qbittorrent', host, port, username, password, enabled)
)
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 500
@settings_bp.route('/settings/transmission', methods=['POST'])
def save_transmission_settings():
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
if session['role'] != 'admin':
return jsonify({'error': 'Admin access required'}), 403
host = request.form.get('host')
port = request.form.get('port')
username = request.form.get('username')
password = request.form.get('password')
enabled = bool(request.form.get('enabled'))
conn = get_db_connection()
try:
# Check if config exists
existing = conn.execute(
"SELECT id FROM clients WHERE name = 'transmission'"
).fetchone()
if existing:
# Update existing config
conn.execute(
"""UPDATE clients SET host = ?, port = ?, username = ?, password = ?, enabled = ?
WHERE name = 'transmission'""",
(host, port, username, password, enabled)
)
else:
# Insert new config
conn.execute(
"""INSERT INTO clients (name, host, port, username, password, enabled)
VALUES (?, ?, ?, ?, ?, ?)""",
('transmission', host, port, username, password, enabled)
)
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 500

217
routes/site.py Normal file
View File

@@ -0,0 +1,217 @@
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
import requests
import sqlite3
site_bp = Blueprint('site', __name__)
def get_db_connection():
conn = sqlite3.connect('pt_manager.db')
conn.row_factory = sqlite3.Row
return conn
def get_nexusphp_config(site_id=None):
conn = get_db_connection()
if site_id:
site = conn.execute("SELECT * FROM sites WHERE id = ?", (site_id,)).fetchone()
conn.close()
if site:
return {
'site_url': site['url'],
'api_token': site['api_token'] if site['api_token'] else ''
}
else:
# Get the first enabled site
site = conn.execute("SELECT * FROM sites WHERE enabled = 1 LIMIT 1").fetchone()
conn.close()
if site:
return {
'site_url': site['url'],
'api_token': site['api_token'] if site['api_token'] else ''
}
# Fallback to default settings
site_url = conn.execute("SELECT value FROM settings WHERE key = 'nexusphp_site_url'").fetchone()
api_token = conn.execute("SELECT value FROM settings WHERE key = 'nexusphp_api_token'").fetchone()
conn.close()
return {
'site_url': site_url['value'] if site_url else 'https://www.ptskit.org',
'api_token': api_token['value'] if api_token else ''
}
def get_all_sites():
conn = get_db_connection()
sites = conn.execute("SELECT * FROM sites WHERE enabled = 1").fetchall()
conn.close()
return sites
@site_bp.route('/site')
def site_index():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
sites = get_all_sites()
return render_template('site/index.html', sites=sites)
@site_bp.route('/site/<int:site_id>/profile')
def profile(site_id):
if 'user_id' not in session:
return redirect(url_for('auth.login'))
sites = get_all_sites()
config = get_nexusphp_config(site_id)
headers = {
'Accept': 'application/json',
# 'Authorization': f'Bearer {config["api_token"]}'
'Authorization': f'Bearer 262|wEGbxaqybJ6ZfLZAAtxX0oiQTymFcelHpc6YHims27a70898'
}
try:
response = requests.get(f'{config["site_url"]}/api/v1/profile', headers=headers)
if response.status_code == 200:
profile_data = response.json()
return render_template('site/profile.html', profile=profile_data, sites=sites, current_site_id=site_id)
else:
return render_template('site/profile.html', error='Failed to fetch profile data', sites=sites, current_site_id=site_id)
except Exception as e:
return render_template('site/profile.html', error=str(e), sites=sites, current_site_id=site_id)
@site_bp.route('/site/<int:site_id>/torrents')
def torrents(site_id):
if 'user_id' not in session:
return redirect(url_for('auth.login'))
sites = get_all_sites()
config = get_nexusphp_config(site_id)
headers = {
'Accept': 'application/json',
# 'Authorization': f'Bearer {config["api_token"]}'
'Authorization': f'Bearer 262|wEGbxaqybJ6ZfLZAAtxX0oiQTymFcelHpc6YHims27a70898'
}
# Get query parameters
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
sort = request.args.get('sort', '-seeders')
title_filter = request.args.get('title', '')
params = {
'page': page,
'per_page': per_page,
'sorts': sort
}
if title_filter:
params['filter[title]'] = title_filter
try:
response = requests.get(f'{config["site_url"]}/api/v1/torrents',
headers=headers, params=params)
if response.status_code == 200:
torrents_data = response.json()
return render_template('site/torrents.html', torrents=torrents_data,
page=page, per_page=per_page, sort=sort, title_filter=title_filter,
sites=sites, current_site_id=site_id)
else:
return render_template('site/torrents.html', error='Failed to fetch torrents data',
sites=sites, current_site_id=site_id)
except Exception as e:
return render_template('site/torrents.html', error=str(e), sites=sites, current_site_id=site_id)
@site_bp.route('/site/<int:site_id>/torrent/<int:torrent_id>')
def torrent_detail(site_id, torrent_id):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
config = get_nexusphp_config(site_id)
headers = {
'Accept': 'application/json',
# 'Authorization': f'Bearer {config["api_token"]}'
'Authorization': f'Bearer 262|wEGbxaqybJ6ZfLZAAtxX0oiQTymFcelHpc6YHims27a70898'
}
try:
response = requests.get(f'{config["site_url"]}/api/v1/detail/{torrent_id}?includes=user,tags&include_fields[torrent]=description,download_url', headers=headers)
if response.status_code == 200:
torrent_data = response.json()
return render_template('site/torrent_detail.html', torrent=torrent_data)
else:
return jsonify({'error': 'Failed to fetch torrent data'}), response.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500
@site_bp.route('/site/<int:site_id>/bookmarks')
def bookmarks(site_id):
if 'user_id' not in session:
return redirect(url_for('auth.login'))
sites = get_all_sites()
config = get_nexusphp_config(site_id)
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {config["api_token"]}'
}
try:
response = requests.get(f'{config["site_url"]}/api/v1/bookmarks', headers=headers)
if response.status_code == 200:
bookmarks_data = response.json()
return render_template('site/bookmarks.html', bookmarks=bookmarks_data,
sites=sites, current_site_id=site_id)
else:
return render_template('site/bookmarks.html', error='Failed to fetch bookmarks data',
sites=sites, current_site_id=site_id)
except Exception as e:
return render_template('site/bookmarks.html', error=str(e), sites=sites, current_site_id=site_id)
@site_bp.route('/site/<int:site_id>/bookmark/add/<int:torrent_id>', methods=['POST'])
def add_bookmark(site_id, torrent_id):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
config = get_nexusphp_config(site_id)
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {config["api_token"]}'
}
data = {
'torrent_id': torrent_id
}
try:
response = requests.post(f'{config["site_url"]}/api/v1/bookmarks',
headers=headers, data=data)
if response.status_code == 200:
return jsonify({'success': True})
else:
return jsonify({'error': 'Failed to add bookmark'}), response.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500
@site_bp.route('/site/<int:site_id>/bookmark/remove/<int:torrent_id>', methods=['POST'])
def remove_bookmark(site_id, torrent_id):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
config = get_nexusphp_config(site_id)
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {config["api_token"]}'
}
data = {
'torrent_id': torrent_id
}
try:
response = requests.post(f'{config["site_url"]}/api/v1/bookmarks/delete',
headers=headers, data=data)
if response.status_code == 200:
return jsonify({'success': True})
else:
return jsonify({'error': 'Failed to remove bookmark'}), response.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500

121
routes/transmission.py Normal file
View File

@@ -0,0 +1,121 @@
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
import sqlite3
import transmission_rpc
import sys
import os
# Import format functions
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from utils.format import format_file_size, format_status
transmission_bp = Blueprint('transmission', __name__)
def get_db_connection():
conn = sqlite3.connect('pt_manager.db')
conn.row_factory = sqlite3.Row
return conn
def get_transmission_client():
conn = get_db_connection()
client_config = conn.execute(
"SELECT * FROM clients WHERE name = 'transmission' AND enabled = 1"
).fetchone()
conn.close()
if not client_config:
return None
try:
tc = transmission_rpc.Client(
host=client_config['host'],
port=client_config['port'],
username=client_config['username'],
password=client_config['password']
)
return tc
except Exception as e:
print(f"Failed to connect to Transmission: {e}")
return None
@transmission_bp.route('/transmission')
def transmission_index():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
return render_template('transmission/index.html')
@transmission_bp.route('/transmission/torrents')
def torrents():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
tc = get_transmission_client()
if not tc:
return render_template('transmission/torrents.html', error='Transmission client not configured or unavailable')
try:
torrents = tc.get_torrents()
# Process torrents to format file sizes and statuses
processed_torrents = []
for torrent in torrents:
# Create a dictionary with the torrent attributes we need
processed_torrent = {
'id': torrent.id,
'name': torrent.name,
'totalSize': format_file_size(torrent.total_size),
'percentDone': torrent.progress,
'status': torrent.status,
'statusString': format_status(torrent.status),
'peersSendingToUs': getattr(torrent, 'peers_sending_to_us', 0),
'peersGettingFromUs': getattr(torrent, 'peers_getting_from_us', 0)
}
processed_torrents.append(processed_torrent)
return render_template('transmission/torrents.html', torrents=processed_torrents)
except Exception as e:
return render_template('transmission/torrents.html', error=f'Failed to fetch torrents: {str(e)}')
@transmission_bp.route('/transmission/torrent/<int:torrent_id>/stop', methods=['POST'])
def stop_torrent(torrent_id):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
tc = get_transmission_client()
if not tc:
return jsonify({'error': 'Transmission client not configured or unavailable'}), 500
try:
tc.stop_torrent(ids=[torrent_id])
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@transmission_bp.route('/transmission/torrent/<int:torrent_id>/start', methods=['POST'])
def start_torrent(torrent_id):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
tc = get_transmission_client()
if not tc:
return jsonify({'error': 'Transmission client not configured or unavailable'}), 500
try:
tc.start_torrent(ids=[torrent_id])
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@transmission_bp.route('/transmission/torrent/<int:torrent_id>/remove', methods=['POST'])
def remove_torrent(torrent_id):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
tc = get_transmission_client()
if not tc:
return jsonify({'error': 'Transmission client not configured or unavailable'}), 500
try:
# Remove torrent and data
tc.remove_torrent(ids=[torrent_id], delete_data=True)
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500

79
routes/user.py Normal file
View File

@@ -0,0 +1,79 @@
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
import sqlite3
import secrets
user_bp = Blueprint('user', __name__)
def get_db_connection():
conn = sqlite3.connect('pt_manager.db')
conn.row_factory = sqlite3.Row
return conn
@user_bp.route('/user')
def user_index():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
if session['role'] != 'admin':
return redirect(url_for('main.index'))
conn = get_db_connection()
users = conn.execute('SELECT id, username, role, created_at FROM users').fetchall()
conn.close()
return render_template('user/index.html', users=users)
@user_bp.route('/user/add', methods=['POST'])
def add_user():
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
if session['role'] != 'admin':
return jsonify({'error': 'Admin access required'}), 403
username = request.form.get('username')
role = request.form.get('role', 'user')
if not username:
return jsonify({'error': 'Username is required'}), 400
# Generate a random password
password = secrets.token_hex(8)
conn = get_db_connection()
try:
conn.execute(
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)',
(username, password, role)
)
conn.commit()
conn.close()
return jsonify({'success': True, 'username': username, 'password': password})
except sqlite3.IntegrityError:
conn.close()
return jsonify({'error': 'Username already exists'}), 400
except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 500
@user_bp.route('/user/delete/<int:user_id>', methods=['POST'])
def delete_user(user_id):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
if session['role'] != 'admin':
return jsonify({'error': 'Admin access required'}), 403
# Prevent deleting oneself
if user_id == session['user_id']:
return jsonify({'error': 'Cannot delete yourself'}), 400
conn = get_db_connection()
try:
conn.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 500