feat: 新增平台

This commit is contained in:
2025-11-30 19:49:25 +08:00
parent c3e56a954d
commit fbd2c491b2
41 changed files with 4293 additions and 76 deletions

View File

@@ -113,6 +113,8 @@ def users_page():
@admin_required
def get_users():
"""获取用户列表API"""
from models import UserGroupExpiry
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
group_id = request.args.get('group_id', type=int)
@@ -126,16 +128,28 @@ def get_users():
page=page, per_page=per_page, error_out=False
)
users = [{
'id': u.id,
'username': u.username,
'email': u.email,
'group_id': u.group_id,
'group_name': u.group.name if u.group else '',
'total_parse_count': u.total_parse_count,
'is_active': u.is_active,
'created_at': u.created_at.isoformat()
} for u in pagination.items]
users = []
for u in pagination.items:
# 获取套餐到期时间
expiry = UserGroupExpiry.query.filter_by(user_id=u.id).first()
expires_at = None
is_expired = False
if expiry and expiry.expires_at:
expires_at = expiry.expires_at.strftime('%Y-%m-%dT%H:%M')
is_expired = expiry.expires_at < datetime.utcnow()
users.append({
'id': u.id,
'username': u.username,
'email': u.email,
'group_id': u.group_id,
'group_name': u.group.name if u.group else '',
'total_parse_count': u.total_parse_count,
'is_active': u.is_active,
'created_at': u.created_at.isoformat(),
'expires_at': expires_at,
'is_expired': is_expired
})
return jsonify({
'success': True,
@@ -152,6 +166,8 @@ def get_users():
@admin_required
def update_user(user_id):
"""更新用户信息"""
from models import UserGroupExpiry
user = User.query.get_or_404(user_id)
data = request.get_json()
@@ -160,6 +176,29 @@ def update_user(user_id):
if 'is_active' in data:
user.is_active = data['is_active']
# 处理套餐到期时间
if 'expires_at' in data:
expiry = UserGroupExpiry.query.filter_by(user_id=user_id).first()
if data['expires_at']:
# 解析时间字符串
expires_at = datetime.strptime(data['expires_at'], '%Y-%m-%dT%H:%M')
if expiry:
expiry.group_id = user.group_id
expiry.expires_at = expires_at
else:
expiry = UserGroupExpiry(
user_id=user_id,
group_id=user.group_id,
expires_at=expires_at
)
db.session.add(expiry)
else:
# 清空到期时间(游客/普通用户)
if expiry:
db.session.delete(expiry)
db.session.commit()
return jsonify({'success': True, 'message': '更新成功'})
@@ -766,3 +805,230 @@ def change_email():
admin.email = email
db.session.commit()
return jsonify({'success': True, 'message': '邮箱修改成功'})
# ==================== 用户组 API ====================
@admin_bp.route('/api/user-groups', methods=['GET'])
@admin_required
def get_user_groups():
"""获取用户组列表(仅返回可兑换的套餐,排除游客和普通用户)"""
# 排除游客(id=1)和普通用户(id=2)
groups = UserGroup.query.filter(UserGroup.id > 2).all()
return jsonify({
'success': True,
'data': [{'id': g.id, 'name': g.name, 'daily_limit': g.daily_limit} for g in groups]
})
# ==================== 兑换码管理 ====================
@admin_bp.route('/redeem-codes')
@admin_required
def redeem_codes_page():
"""兑换码管理页面"""
from flask import render_template
return render_template('admin_redeem_codes.html')
@admin_bp.route('/api/redeem-codes', methods=['GET'])
@admin_required
def get_redeem_codes():
"""获取兑换码列表"""
from models import RedeemCode
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
batch_id = request.args.get('batch_id', '')
status = request.args.get('status', '') # unused, used, expired
query = RedeemCode.query
if batch_id:
query = query.filter(RedeemCode.batch_id == batch_id)
if status == 'unused':
query = query.filter(RedeemCode.is_used == False)
elif status == 'used':
query = query.filter(RedeemCode.is_used == True)
elif status == 'expired':
query = query.filter(RedeemCode.is_used == False, RedeemCode.expires_at < datetime.utcnow())
pagination = query.order_by(RedeemCode.created_at.desc()).paginate(page=page, per_page=per_page)
codes = []
for code in pagination.items:
codes.append({
'id': code.id,
'code': code.code,
'batch_id': code.batch_id,
'target_group': code.target_group.name if code.target_group else '',
'target_group_id': code.target_group_id,
'duration_days': code.duration_days,
'is_used': code.is_used,
'used_by': code.user.username if code.user else None,
'used_at': code.used_at.strftime('%Y-%m-%d %H:%M') if code.used_at else None,
'expires_at': code.expires_at.strftime('%Y-%m-%d %H:%M') if code.expires_at else None,
'is_expired': code.expires_at and code.expires_at < datetime.utcnow() and not code.is_used,
'remark': code.remark,
'created_at': code.created_at.strftime('%Y-%m-%d %H:%M')
})
return jsonify({
'success': True,
'data': codes,
'pagination': {
'page': page,
'per_page': per_page,
'total': pagination.total,
'pages': pagination.pages
}
})
@admin_bp.route('/api/redeem-codes/generate', methods=['POST'])
@admin_required
def generate_redeem_codes():
"""批量生成兑换码"""
from models import RedeemCode
import secrets
import string
data = request.get_json()
count = data.get('count', 1)
target_group_id = data.get('target_group_id')
duration_days = data.get('duration_days', 30)
expires_days = data.get('expires_days') # 兑换码有效期(天)
remark = data.get('remark', '')
prefix = data.get('prefix', '') # 兑换码前缀
if not target_group_id:
return jsonify({'success': False, 'message': '请选择目标用户组'}), 400
if count < 1 or count > 1000:
return jsonify({'success': False, 'message': '生成数量必须在1-1000之间'}), 400
# 验证用户组存在
group = UserGroup.query.get(target_group_id)
if not group:
return jsonify({'success': False, 'message': '用户组不存在'}), 400
# 生成批次ID
batch_id = datetime.now().strftime('%Y%m%d%H%M%S') + secrets.token_hex(4)
# 计算过期时间
expires_at = None
if expires_days:
expires_at = datetime.utcnow() + timedelta(days=expires_days)
# 生成兑换码
codes = []
chars = string.ascii_uppercase + string.digits
for _ in range(count):
# 生成随机码
random_part = ''.join(secrets.choice(chars) for _ in range(12))
code_str = f"{prefix}{random_part}" if prefix else random_part
code = RedeemCode(
code=code_str,
batch_id=batch_id,
target_group_id=target_group_id,
duration_days=duration_days,
expires_at=expires_at,
remark=remark
)
db.session.add(code)
codes.append(code_str)
db.session.commit()
return jsonify({
'success': True,
'message': f'成功生成 {count} 个兑换码',
'data': {
'batch_id': batch_id,
'codes': codes
}
})
@admin_bp.route('/api/redeem-codes/<int:code_id>', methods=['DELETE'])
@admin_required
def delete_redeem_code(code_id):
"""删除兑换码"""
from models import RedeemCode
code = RedeemCode.query.get(code_id)
if not code:
return jsonify({'success': False, 'message': '兑换码不存在'}), 404
if code.is_used:
return jsonify({'success': False, 'message': '已使用的兑换码不能删除'}), 400
db.session.delete(code)
db.session.commit()
return jsonify({'success': True, 'message': '删除成功'})
@admin_bp.route('/api/redeem-codes/batch/<batch_id>', methods=['DELETE'])
@admin_required
def delete_batch_codes(batch_id):
"""删除整批兑换码"""
from models import RedeemCode
# 只删除未使用的
deleted = RedeemCode.query.filter_by(batch_id=batch_id, is_used=False).delete()
db.session.commit()
return jsonify({'success': True, 'message': f'成功删除 {deleted} 个未使用的兑换码'})
@admin_bp.route('/api/redeem-codes/batches', methods=['GET'])
@admin_required
def get_batch_list():
"""获取批次列表"""
from models import RedeemCode
batches = db.session.query(
RedeemCode.batch_id,
func.count(RedeemCode.id).label('total'),
func.sum(db.case((RedeemCode.is_used == True, 1), else_=0)).label('used'),
func.min(RedeemCode.created_at).label('created_at')
).group_by(RedeemCode.batch_id).order_by(func.min(RedeemCode.created_at).desc()).all()
result = []
for batch in batches:
result.append({
'batch_id': batch.batch_id,
'total': batch.total,
'used': int(batch.used or 0),
'unused': batch.total - int(batch.used or 0),
'created_at': batch.created_at.strftime('%Y-%m-%d %H:%M') if batch.created_at else ''
})
return jsonify({'success': True, 'data': result})
@admin_bp.route('/api/redeem-codes/export/<batch_id>', methods=['GET'])
@admin_required
def export_batch_codes(batch_id):
"""导出批次兑换码"""
from models import RedeemCode
codes = RedeemCode.query.filter_by(batch_id=batch_id).all()
if not codes:
return jsonify({'success': False, 'message': '批次不存在'}), 404
code_list = [code.code for code in codes]
return jsonify({
'success': True,
'data': {
'batch_id': batch_id,
'codes': code_list,
'total': len(code_list)
}
})

212
routes/api_v1.py Normal file
View File

@@ -0,0 +1,212 @@
from flask import Blueprint, request, jsonify
from models import ParserAPI, ParseLog, UserApiKey, ApiKeyDailyStat, db
from parsers.factory import ParserFactory
from utils.security import get_client_ip
from datetime import datetime, date
import time
api_v1_bp = Blueprint('api_v1', __name__, url_prefix='/api/v1')
def validate_and_get_key(api_key):
"""验证 API Key 并返回 key 对象"""
if not api_key:
return None, 'API Key 不能为空'
key_obj = UserApiKey.query.filter_by(api_key=api_key).first()
if not key_obj:
return None, 'API Key 无效'
if not key_obj.is_active:
return None, 'API Key 已被禁用'
if not key_obj.user.is_active:
return None, '用户账号已被禁用'
# 检查每日限额
today_stat = ApiKeyDailyStat.query.filter_by(
api_key_id=key_obj.id,
date=date.today()
).first()
if today_stat and today_stat.call_count >= key_obj.daily_limit:
return None, f'已达到每日调用限额({key_obj.daily_limit}次)'
return key_obj, None
def record_api_call(key_obj, ip_address, success=True):
"""记录 API 调用"""
key_obj.total_calls = (key_obj.total_calls or 0) + 1
key_obj.last_used_at = datetime.utcnow()
key_obj.last_used_ip = ip_address
today_stat = ApiKeyDailyStat.query.filter_by(
api_key_id=key_obj.id,
date=date.today()
).first()
if not today_stat:
today_stat = ApiKeyDailyStat(api_key_id=key_obj.id, date=date.today())
db.session.add(today_stat)
today_stat.call_count = (today_stat.call_count or 0) + 1
if success:
today_stat.success_count = (today_stat.success_count or 0) + 1
else:
today_stat.fail_count = (today_stat.fail_count or 0) + 1
@api_v1_bp.route('/parse', methods=['GET'])
def parse_video():
"""
对外解析API - 简化版
请求方式: GET
参数:
key: API Key
url: 视频链接
示例: /api/v1/parse?key=sk_xxx&url=https://v.douyin.com/xxx
返回:
{
"code": 200,
"msg": "解析成功",
"data": {
"cover": "封面URL",
"title": "标题",
"description": "简介",
"author": "作者",
"video_url": "无水印视频链接"
}
}
"""
# 获取参数
api_key = request.args.get('key')
video_url = request.args.get('url')
# 验证 API Key
key_obj, error = validate_and_get_key(api_key)
if error:
return jsonify({'code': 401, 'msg': error})
# 验证 URL
if not video_url:
return jsonify({'code': 400, 'msg': '请提供视频链接'})
# 检测平台
try:
platform = ParserFactory.detect_platform(video_url)
except ValueError as e:
return jsonify({'code': 400, 'msg': str(e)})
# 展开短链接
video_url = ParserFactory.expand_short_url(video_url)
# 获取客户端IP
ip_address = get_client_ip(request)
start_time = time.time()
# 获取该平台所有可用的API
available_apis = ParserAPI.query.filter_by(
platform=platform.lower(),
is_enabled=True
).all()
if not available_apis:
return jsonify({'code': 503, 'msg': f'没有可用的{platform}解析接口'})
last_error = None
user_id = key_obj.user_id
# 尝试所有可用的APIfailover机制
for api_config in available_apis:
try:
parser = ParserFactory.create_parser(api_config)
result = parser.parse(video_url)
# 验证解析结果video_url 不能为空
if not result.get('video_url'):
raise Exception('未能获取到视频链接')
response_time = int((time.time() - start_time) * 1000)
# 记录成功日志
log = ParseLog(
user_id=user_id,
ip_address=ip_address,
platform=platform,
video_url=video_url,
parser_api_id=api_config.id,
status='success',
response_time=response_time
)
db.session.add(log)
# 更新API统计
api_config.total_calls = (api_config.total_calls or 0) + 1
api_config.success_calls = (api_config.success_calls or 0) + 1
avg_time = api_config.avg_response_time or 0
api_config.avg_response_time = int(
(avg_time * (api_config.total_calls - 1) + response_time) / api_config.total_calls
)
api_config.fail_count = 0
# 记录 API Key 调用
record_api_call(key_obj, ip_address, success=True)
db.session.commit()
return jsonify({
'code': 200,
'msg': '解析成功',
'data': {
'cover': result.get('cover', ''),
'title': result.get('title', ''),
'description': result.get('description', ''),
'author': result.get('author', ''),
'video_url': result.get('video_url', '')
}
})
except Exception as e:
last_error = str(e)
api_config.total_calls = (api_config.total_calls or 0) + 1
api_config.fail_count = (api_config.fail_count or 0) + 1
db.session.commit()
continue
# 所有API都失败
response_time = int((time.time() - start_time) * 1000)
log = ParseLog(
user_id=user_id,
ip_address=ip_address,
platform=platform,
video_url=video_url,
status='failed',
error_message=last_error or '所有接口都失败',
response_time=response_time
)
db.session.add(log)
record_api_call(key_obj, ip_address, success=False)
db.session.commit()
return jsonify({
'code': 500,
'msg': last_error or '解析失败,请稍后重试'
})
@api_v1_bp.route('/platforms', methods=['GET'])
def get_platforms():
"""获取支持的平台列表"""
return jsonify({
'code': 200,
'msg': '获取成功',
'data': [
{'name': 'douyin', 'display_name': '抖音'},
{'name': 'tiktok', 'display_name': 'TikTok'},
{'name': 'bilibili', 'display_name': '哔哩哔哩'},
{'name': 'kuaishou', 'display_name': '快手'},
{'name': 'pipixia', 'display_name': '皮皮虾'},
{'name': 'weibo', 'display_name': '微博'}
]
})

174
routes/apikey.py Normal file
View File

@@ -0,0 +1,174 @@
from flask import Blueprint, request, jsonify, render_template
from flask_login import login_required, current_user
from models import db, UserApiKey, ApiKeyDailyStat
from datetime import datetime, date
import secrets
apikey_bp = Blueprint('apikey', __name__, url_prefix='/user/apikey')
def generate_api_key():
"""生成 API Key"""
return f"sk_{secrets.token_hex(24)}"
@apikey_bp.route('/')
@login_required
def manage_page():
"""API Key 管理页面"""
return render_template('apikey/manage.html')
@apikey_bp.route('/list', methods=['GET'])
@login_required
def list_keys():
"""获取用户的 API Key 列表"""
keys = UserApiKey.query.filter_by(user_id=current_user.id).order_by(UserApiKey.created_at.desc()).all()
result = []
for key in keys:
# 获取今日调用次数
today_stat = ApiKeyDailyStat.query.filter_by(
api_key_id=key.id,
date=date.today()
).first()
result.append({
'id': key.id,
'name': key.name,
'api_key': key.api_key[:12] + '...' + key.api_key[-4:], # 隐藏中间部分
'is_active': key.is_active,
'daily_limit': key.daily_limit,
'total_calls': key.total_calls,
'today_calls': today_stat.call_count if today_stat else 0,
'last_used_at': key.last_used_at.strftime('%Y-%m-%d %H:%M:%S') if key.last_used_at else None,
'created_at': key.created_at.strftime('%Y-%m-%d %H:%M:%S')
})
return jsonify({
'success': True,
'data': result
})
@apikey_bp.route('/create', methods=['POST'])
@login_required
def create_key():
"""创建新的 API Key"""
data = request.get_json()
name = data.get('name', '').strip()
if not name:
return jsonify({'success': False, 'message': '请输入 Key 名称'}), 400
if len(name) > 100:
return jsonify({'success': False, 'message': 'Key 名称不能超过100个字符'}), 400
# 检查用户已有的 Key 数量限制每个用户最多5个
key_count = UserApiKey.query.filter_by(user_id=current_user.id).count()
if key_count >= 5:
return jsonify({'success': False, 'message': '每个用户最多创建5个 API Key'}), 400
# 生成新的 API Key
api_key = generate_api_key()
# 根据用户等级设置每日限制
daily_limit = 100 # 默认
if current_user.group:
if current_user.group.id == 3: # VIP
daily_limit = 500
elif current_user.group.id == 4: # SVIP
daily_limit = 2000
new_key = UserApiKey(
user_id=current_user.id,
name=name,
api_key=api_key,
daily_limit=daily_limit
)
db.session.add(new_key)
db.session.commit()
return jsonify({
'success': True,
'message': 'API Key 创建成功',
'data': {
'id': new_key.id,
'name': new_key.name,
'api_key': api_key, # 只在创建时返回完整的 Key
'daily_limit': new_key.daily_limit
}
})
@apikey_bp.route('/delete/<int:key_id>', methods=['DELETE'])
@login_required
def delete_key(key_id):
"""删除 API Key"""
key = UserApiKey.query.filter_by(id=key_id, user_id=current_user.id).first()
if not key:
return jsonify({'success': False, 'message': 'API Key 不存在'}), 404
db.session.delete(key)
db.session.commit()
return jsonify({
'success': True,
'message': 'API Key 已删除'
})
@apikey_bp.route('/toggle/<int:key_id>', methods=['POST'])
@login_required
def toggle_key(key_id):
"""启用/禁用 API Key"""
key = UserApiKey.query.filter_by(id=key_id, user_id=current_user.id).first()
if not key:
return jsonify({'success': False, 'message': 'API Key 不存在'}), 404
key.is_active = not key.is_active
db.session.commit()
return jsonify({
'success': True,
'message': f"API Key 已{'启用' if key.is_active else '禁用'}",
'is_active': key.is_active
})
@apikey_bp.route('/stats/<int:key_id>', methods=['GET'])
@login_required
def get_stats(key_id):
"""获取 API Key 统计数据"""
key = UserApiKey.query.filter_by(id=key_id, user_id=current_user.id).first()
if not key:
return jsonify({'success': False, 'message': 'API Key 不存在'}), 404
# 获取最近7天的统计
from datetime import timedelta
stats = ApiKeyDailyStat.query.filter(
ApiKeyDailyStat.api_key_id == key_id,
ApiKeyDailyStat.date >= date.today() - timedelta(days=6)
).order_by(ApiKeyDailyStat.date.asc()).all()
result = []
for stat in stats:
result.append({
'date': stat.date.strftime('%Y-%m-%d'),
'call_count': stat.call_count,
'success_count': stat.success_count,
'fail_count': stat.fail_count
})
return jsonify({
'success': True,
'data': {
'key_name': key.name,
'total_calls': key.total_calls,
'daily_stats': result
}
})

View File

@@ -219,14 +219,22 @@ def profile():
@login_required
def get_profile():
"""获取用户个人中心数据"""
from models import UserGroup, DailyParseStat, ParseLog
from datetime import date
from models import UserGroup, DailyParseStat, ParseLog, UserGroupExpiry
from datetime import date, datetime
# 获取用户组信息
user_group = UserGroup.query.get(current_user.group_id)
daily_limit = user_group.daily_limit if user_group else 10
group_name = user_group.name if user_group else '普通用户'
# 获取套餐到期时间
group_expiry = UserGroupExpiry.query.filter_by(user_id=current_user.id).first()
expires_at = None
is_expired = False
if group_expiry:
expires_at = group_expiry.expires_at.strftime('%Y-%m-%d %H:%M') if group_expiry.expires_at else None
is_expired = group_expiry.expires_at < datetime.utcnow() if group_expiry.expires_at else False
# 获取今日使用次数
today = date.today()
today_stat = DailyParseStat.query.filter_by(
@@ -263,7 +271,9 @@ def get_profile():
'group': {
'id': current_user.group_id,
'name': group_name,
'daily_limit': daily_limit
'daily_limit': daily_limit,
'expires_at': expires_at,
'is_expired': is_expired
},
'usage': {
'daily_limit': daily_limit,
@@ -274,3 +284,72 @@ def get_profile():
'parse_logs': logs_data
}
})
@auth_bp.route('/api/redeem', methods=['POST'])
@login_required
def redeem_code():
"""用户兑换码"""
from models import RedeemCode, UserGroupExpiry
from datetime import datetime, timedelta
data = request.get_json()
code_str = data.get('code', '').strip().upper()
if not code_str:
return jsonify({'success': False, 'message': '请输入兑换码'}), 400
# 查找兑换码
code = RedeemCode.query.filter_by(code=code_str).first()
if not code:
return jsonify({'success': False, 'message': '兑换码不存在'}), 404
if code.is_used:
return jsonify({'success': False, 'message': '兑换码已被使用'}), 400
if code.expires_at and code.expires_at < datetime.utcnow():
return jsonify({'success': False, 'message': '兑换码已过期'}), 400
# 执行兑换
user = current_user
# 更新用户组
old_group_id = user.group_id
user.group_id = code.target_group_id
# 计算到期时间
expires_at = datetime.utcnow() + timedelta(days=code.duration_days)
# 更新或创建到期记录
expiry = UserGroupExpiry.query.filter_by(user_id=user.id).first()
if expiry:
# 如果当前套餐未过期且是同一套餐,则叠加时间
if expiry.group_id == code.target_group_id and expiry.expires_at > datetime.utcnow():
expires_at = expiry.expires_at + timedelta(days=code.duration_days)
expiry.group_id = code.target_group_id
expiry.expires_at = expires_at
else:
expiry = UserGroupExpiry(
user_id=user.id,
group_id=code.target_group_id,
expires_at=expires_at
)
db.session.add(expiry)
# 标记兑换码已使用
code.is_used = True
code.used_by = user.id
code.used_at = datetime.utcnow()
db.session.commit()
return jsonify({
'success': True,
'message': f'兑换成功!您已升级为 {code.target_group.name},有效期至 {expires_at.strftime("%Y-%m-%d %H:%M")}',
'data': {
'group_name': code.target_group.name,
'expires_at': expires_at.strftime('%Y-%m-%d %H:%M'),
'duration_days': code.duration_days
}
})

View File

@@ -1,4 +1,5 @@
from flask import Blueprint, render_template, redirect
from flask import Blueprint, render_template, redirect, request, Response
import requests
main_bp = Blueprint('main', __name__)
@@ -11,6 +12,12 @@ def index():
config[c.config_key] = c.config_value
return render_template('index.html', config=config)
@main_bp.route('/api-docs')
def api_docs():
"""API 文档页面"""
return render_template('api_docs.html')
@main_bp.route('/favicon.ico')
def favicon():
"""处理 favicon 请求"""
@@ -24,3 +31,106 @@ def favicon():
return redirect(logo_config.config_value)
# 返回空响应避免 404
return '', 204
@main_bp.route('/proxy/download')
def proxy_download():
"""代理下载视频/图片,绕过防盗链,支持 Range 请求"""
url = request.args.get('url')
if not url:
return 'Missing url parameter', 400
# 限制只能代理特定域名
allowed_domains = [
'weibocdn.com', 'sinaimg.cn', 'weibo.com',
'ppxvod.com', 'byteimg.com', 'pipix.com', '365yg.com', 'ixigua.com',
'kwimgs.com', 'kwaicdn.com', 'kuaishou.com', 'gifshow.com', 'yximgs.com',
'douyinvod.com', 'douyin.com', 'tiktokcdn.com', 'bytedance.com',
'bilivideo.com', 'bilibili.com', 'hdslb.com',
'zjcdn.com', 'douyinpic.com'
]
from urllib.parse import urlparse
parsed = urlparse(url)
domain = parsed.netloc.lower()
is_allowed = any(d in domain for d in allowed_domains)
if not is_allowed:
return f'Domain not allowed: {domain}', 403
# 根据域名设置不同的 Referer
referer_map = {
'weibo': 'https://weibo.com/',
'sina': 'https://weibo.com/',
'pipix': 'https://www.pipix.com/',
'365yg': 'https://www.pipix.com/',
'ixigua': 'https://www.ixigua.com/',
'kuaishou': 'https://www.kuaishou.com/',
'kwimgs': 'https://www.kuaishou.com/',
'kwaicdn': 'https://www.kuaishou.com/',
'yximgs': 'https://www.kuaishou.com/',
'douyin': 'https://www.douyin.com/',
'bytedance': 'https://www.douyin.com/',
'zjcdn': 'https://www.douyin.com/',
'douyinpic': 'https://www.douyin.com/',
'bilibili': 'https://www.bilibili.com/',
'hdslb': 'https://www.bilibili.com/',
}
referer = 'https://www.google.com/'
for key, ref in referer_map.items():
if key in domain:
referer = ref
break
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': referer
}
# 转发 Range 请求头,支持视频进度条拖动
range_header = request.headers.get('Range')
if range_header:
headers['Range'] = range_header
resp = requests.get(url, headers=headers, stream=True, timeout=30)
# 获取文件类型
content_type = resp.headers.get('Content-Type', 'application/octet-stream')
# 设置下载文件名
if 'video' in content_type:
filename = f'video_{int(__import__("time").time())}.mp4'
elif 'image' in content_type:
filename = f'image_{int(__import__("time").time())}.jpg'
else:
filename = f'file_{int(__import__("time").time())}'
def generate():
for chunk in resp.iter_content(chunk_size=8192):
yield chunk
# 构建响应头
response_headers = {
'Content-Type': content_type,
'Accept-Ranges': 'bytes'
}
# 转发相关响应头
if resp.headers.get('Content-Length'):
response_headers['Content-Length'] = resp.headers.get('Content-Length')
if resp.headers.get('Content-Range'):
response_headers['Content-Range'] = resp.headers.get('Content-Range')
# 根据是否是 Range 请求返回不同状态码
status_code = resp.status_code
return Response(
generate(),
status=status_code,
headers=response_headers
)
except Exception as e:
return f'Download failed: {str(e)}', 500

View File

@@ -40,6 +40,9 @@ def parse_video():
except ValueError as e:
return jsonify({'success': False, 'message': str(e)}), 400
# 展开短链接
video_url = ParserFactory.expand_short_url(video_url)
# 生成任务ID
task_id = str(uuid.uuid4())
@@ -116,6 +119,10 @@ def _process_task(task_id, video_url, platform, user_id, ip_address):
# 执行解析
result = parser.parse(video_url)
# 验证解析结果video_url 不能为空
if not result.get('video_url'):
raise Exception('未能获取到视频链接')
# 计算响应时间
response_time = int((time.time() - start_time) * 1000)