Merge pull request #39 from x1ao4/dev

新增影视发现功能
This commit is contained in:
x1ao4 2025-07-14 00:32:28 +08:00 committed by GitHub
commit 1ce1ffe24f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 3118 additions and 111 deletions

View File

@ -13,6 +13,7 @@
- **Aria2**:支持成功添加 Aria2 下载任务后自动删除夸克网盘内对应的文件,清理网盘空间。
- **文件整理**:支持浏览和管理多个夸克账号的网盘文件,支持单项/批量重命名(支持应用完整的命名、过滤规则和撤销重命名等操作)、移动文件、删除文件、新建文件夹等操作。
- **更新状态**:支持在任务列表页面显示任务的最近更新日期、最近转存文件,支持在任务列表、转存记录、文件整理页面显示当日更新标识(对于当日更新的内容)。
- **影视发现**:支持在影视发现页面浏览豆瓣热门影视榜单,一键快速创建任务,智能填充任务配置,实现便捷订阅。
本项目修改后的版本为个人需求定制版,目的是满足我自己的使用需求,某些(我不用的)功能可能会因为修改而出现 BUG不一定会被修复。若你要使用本项目请知晓本人不是程序员我无法保证本项目的稳定性如果你在使用过程中发现了 BUG可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。
@ -48,6 +49,7 @@
- [x] 任务结束期限,期限后不执行此任务
- [x] 可单独指定子任务星期几执行
- [x] **支持通过任务名称跳转 TMDB、豆瓣相关搜索页面**
- [x] **支持通过影视发现页面浏览豆瓣热门影视榜单、快速创建任务(支持智能填充任务配置)**
- 媒体库整合
- [x] 根据任务名搜索 Emby 媒体库

View File

@ -40,6 +40,9 @@ from quark_auto_save import Config, format_bytes
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from quark_auto_save import extract_episode_number, sort_file_by_name, chinese_to_arabic, is_date_format
# 导入豆瓣服务
from sdk.douban_service import douban_service
def process_season_episode_info(filename, task_name=None):
"""
@ -309,7 +312,7 @@ def is_login():
@app.route("/favicon.ico")
def favicon():
return send_from_directory(
os.path.join(app.root_path, "static"),
os.path.join(app.root_path, "static", "images"),
"favicon.ico",
mimetype="image/vnd.microsoft.icon",
)
@ -2277,6 +2280,107 @@ def has_rename_record():
return jsonify({"has_rename": has_rename})
# 豆瓣API路由
# 通用电影接口
@app.route("/api/douban/movie/recent_hot")
def get_movie_recent_hot():
"""获取电影榜单 - 通用接口"""
try:
category = request.args.get('category', '热门')
type_param = request.args.get('type', '全部')
limit = int(request.args.get('limit', 20))
start = int(request.args.get('start', 0))
# 映射category到main_category
category_mapping = {
'热门': 'movie_hot',
'最新': 'movie_latest',
'豆瓣高分': 'movie_top',
'冷门佳片': 'movie_underrated'
}
main_category = category_mapping.get(category, 'movie_hot')
result = douban_service.get_list_data(main_category, type_param, limit, start)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'获取电影榜单失败: {str(e)}',
'data': {'items': []}
})
@app.route("/api/douban/movie/<movie_type>/<sub_category>")
def get_movie_list(movie_type, sub_category):
"""获取电影榜单"""
try:
limit = int(request.args.get('limit', 20))
start = int(request.args.get('start', 0))
main_category = f"movie_{movie_type}"
result = douban_service.get_list_data(main_category, sub_category, limit, start)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'获取电影榜单失败: {str(e)}',
'data': {'items': []}
})
# 通用电视剧接口
@app.route("/api/douban/tv/recent_hot")
def get_tv_recent_hot():
"""获取电视剧榜单 - 通用接口"""
try:
category = request.args.get('category', 'tv')
type_param = request.args.get('type', 'tv')
limit = int(request.args.get('limit', 20))
start = int(request.args.get('start', 0))
# 映射category到main_category
if category == 'tv':
main_category = 'tv_drama'
elif category == 'show':
main_category = 'tv_variety'
elif category == 'tv_drama':
main_category = 'tv_drama'
elif category == 'tv_variety':
main_category = 'tv_variety'
else:
main_category = 'tv_drama'
result = douban_service.get_list_data(main_category, type_param, limit, start)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'获取电视剧榜单失败: {str(e)}',
'data': {'items': []}
})
@app.route("/api/douban/tv/<tv_type>/<sub_category>")
def get_tv_list(tv_type, sub_category):
"""获取电视剧/综艺榜单"""
try:
limit = int(request.args.get('limit', 20))
start = int(request.args.get('start', 0))
main_category = f"tv_{tv_type}"
result = douban_service.get_list_data(main_category, sub_category, limit, start)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'获取电视剧榜单失败: {str(e)}',
'data': {'items': []}
})
if __name__ == "__main__":
init()
reload_tasks()

514
app/sdk/douban_service.py Normal file
View File

@ -0,0 +1,514 @@
# -*- coding: utf-8 -*-
"""
豆瓣API服务模块
用于获取豆瓣影视榜单数据
"""
import requests
from typing import Dict, Optional, Any
class DoubanService:
"""豆瓣API服务类"""
def __init__(self):
self.base_url = "https://m.douban.com/rexxar/api/v2"
self.headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1',
'Referer': 'https://m.douban.com/',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate', # 移除br避免Brotli压缩
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
}
# 电影榜单配置 - 4个大类每个大类下有5个小类
self.movie_categories = {
'热门电影': {
'全部': {'category': '热门', 'type': '全部'},
'华语': {'category': '热门', 'type': '华语'},
'欧美': {'category': '热门', 'type': '欧美'},
'韩国': {'category': '热门', 'type': '韩国'},
'日本': {'category': '热门', 'type': '日本'}
},
'最新电影': {
'全部': {'category': '最新', 'type': '全部'},
'华语': {'category': '最新', 'type': '华语'},
'欧美': {'category': '最新', 'type': '欧美'},
'韩国': {'category': '最新', 'type': '韩国'},
'日本': {'category': '最新', 'type': '日本'}
},
'豆瓣高分': {
'全部': {'category': '豆瓣高分', 'type': '全部'},
'华语': {'category': '豆瓣高分', 'type': '华语'},
'欧美': {'category': '豆瓣高分', 'type': '欧美'},
'韩国': {'category': '豆瓣高分', 'type': '韩国'},
'日本': {'category': '豆瓣高分', 'type': '日本'}
},
'冷门佳片': {
'全部': {'category': '冷门佳片', 'type': '全部'},
'华语': {'category': '冷门佳片', 'type': '华语'},
'欧美': {'category': '冷门佳片', 'type': '欧美'},
'韩国': {'category': '冷门佳片', 'type': '韩国'},
'日本': {'category': '冷门佳片', 'type': '日本'}
}
}
# 剧集榜单配置 - 2个大类
self.tv_categories = {
'最近热门剧集': {
'综合': {'category': 'tv', 'type': 'tv'},
'国产剧': {'category': 'tv', 'type': 'tv_domestic'},
'欧美剧': {'category': 'tv', 'type': 'tv_american'},
'日剧': {'category': 'tv', 'type': 'tv_japanese'},
'韩剧': {'category': 'tv', 'type': 'tv_korean'},
'动画': {'category': 'tv', 'type': 'tv_animation'},
'纪录片': {'category': 'tv', 'type': 'tv_documentary'}
},
'最近热门综艺': {
'综合': {'category': 'show', 'type': 'show'},
'国内': {'category': 'show', 'type': 'show_domestic'},
'国外': {'category': 'show', 'type': 'show_foreign'}
}
}
def get_list_data(self, main_category: str, sub_category: str, limit: int = 20, start: int = 0) -> Dict[str, Any]:
"""
获取榜单数据
Args:
main_category: 主分类 (movie_hot, movie_latest, etc.)
sub_category: 子分类 (全部, 华语, 欧美, etc.)
limit: 返回数量限制
start: 起始位置
Returns:
包含榜单数据的字典
"""
try:
# 判断是电影还是电视剧
if main_category.startswith('movie_'):
return self._get_movie_ranking(main_category, sub_category, start, limit)
elif main_category.startswith('tv_'):
return self._get_tv_ranking(main_category, sub_category, start, limit)
else:
return {
'success': False,
'message': f'不支持的主分类: {main_category}',
'data': {'items': []}
}
except Exception as e:
return {
'success': False,
'message': f'获取榜单数据失败: {str(e)}',
'data': {'items': []}
}
def _get_movie_ranking(self, main_category: str, sub_category: str, start: int = 0, limit: int = 20) -> Dict[str, Any]:
"""获取电影榜单数据"""
try:
# 映射主分类到豆瓣分类
category_mapping = {
'movie_hot': '热门电影',
'movie_latest': '最新电影',
'movie_top': '豆瓣高分',
'movie_underrated': '冷门佳片'
}
douban_main_category = category_mapping.get(main_category, '热门电影')
# 获取对应的category和type
if douban_main_category not in self.movie_categories:
# 使用模拟数据
mock_data = self._get_mock_movie_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': '不支持的分类,使用模拟数据'
}
}
category_config = self.movie_categories[douban_main_category].get(sub_category)
if not category_config:
# 使用模拟数据
mock_data = self._get_mock_movie_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': '不支持的子分类,使用模拟数据'
}
}
# 构建请求参数 - 按照参考项目的逻辑处理分类参数
params = {'start': start, 'limit': limit}
# 根据分类映射到豆瓣API的参数
douban_category = category_config.get('category', '热门')
douban_type = category_config.get('type', '全部')
# 根据不同的category和type添加特定参数完全按照参考项目的逻辑
if douban_category != '热门' or douban_type != '全部':
if douban_type != '全部':
params['type'] = douban_type
if douban_category != '热门':
params['category'] = douban_category
# 尝试调用豆瓣API
try:
url = f"{self.base_url}/subject/recent_hot/movie"
# 创建session来自动处理gzip解压
session = requests.Session()
session.headers.update(self.headers)
response = session.get(url, params=params, timeout=30)
response.raise_for_status()
# 检查响应内容是否为空
if not response.text.strip():
raise ValueError("API返回空响应")
data = response.json()
# 处理豆瓣移动端API的响应格式
items = data.get('items', []) or data.get('subjects', [])
if not items:
mock_data = self._get_mock_movie_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': 'API返回空数据'
}
}
# 处理返回的数据
processed_items = []
for item in items[:limit]:
processed_item = self._process_item(item)
if processed_item:
processed_items.append(processed_item)
return {
'success': True,
'message': '获取成功',
'data': {
'items': processed_items,
'total': data.get('total', len(processed_items))
}
}
except Exception:
mock_data = self._get_mock_movie_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': 'API调用失败'
}
}
except Exception as e:
mock_data = self._get_mock_movie_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': f'获取失败: {str(e)}'
}
}
def _get_tv_ranking(self, main_category: str, sub_category: str, start: int = 0, limit: int = 20) -> Dict[str, Any]:
"""获取电视剧榜单数据"""
try:
# 映射主分类到豆瓣分类
category_mapping = {
'tv_drama': '最近热门剧集',
'tv_variety': '最近热门综艺'
}
douban_main_category = category_mapping.get(main_category, '最近热门剧集')
# 获取对应的category和type
if douban_main_category not in self.tv_categories:
# 使用模拟数据
mock_data = self._get_mock_tv_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': '不支持的分类,使用模拟数据'
}
}
category_config = self.tv_categories[douban_main_category].get(sub_category)
if not category_config:
# 使用模拟数据
mock_data = self._get_mock_tv_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': '不支持的子分类,使用模拟数据'
}
}
# 构建请求参数 - 按照参考项目的逻辑处理分类参数
params = {'start': start, 'limit': limit}
# 根据分类映射到豆瓣API的参数
douban_category = category_config.get('category', 'tv')
douban_type = category_config.get('type', 'tv')
# 根据不同的category和type添加特定参数完全按照参考项目的逻辑
if douban_category != 'tv' or douban_type != 'tv':
if douban_type != 'tv':
params['type'] = douban_type
if douban_category != 'tv':
params['category'] = douban_category
# 尝试调用豆瓣API
try:
url = f"{self.base_url}/subject/recent_hot/tv"
# 创建session来自动处理gzip解压
session = requests.Session()
session.headers.update(self.headers)
response = session.get(url, params=params, timeout=30)
response.raise_for_status()
# 检查响应内容是否为空
if not response.text.strip():
raise ValueError("TV API返回空响应")
data = response.json()
# 处理豆瓣移动端API的响应格式
items = data.get('items', []) or data.get('subjects', [])
if not items:
mock_data = self._get_mock_tv_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': 'API返回空数据'
}
}
# 处理返回的数据
processed_items = []
for item in items[:limit]:
processed_item = self._process_item(item)
if processed_item:
processed_items.append(processed_item)
return {
'success': True,
'message': '获取成功',
'data': {
'items': processed_items,
'total': data.get('total', len(processed_items))
}
}
except Exception:
mock_data = self._get_mock_tv_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': 'API调用失败'
}
}
except Exception as e:
mock_data = self._get_mock_tv_data()
return {
'success': True,
'message': '获取成功(模拟数据)',
'data': {
'items': mock_data['items'][:limit],
'total': len(mock_data['items']),
'is_mock_data': True,
'mock_reason': f'获取失败: {str(e)}'
}
}
def _process_item(self, item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
处理单个影片数据项
Args:
item: 原始数据项
Returns:
处理后的数据项
"""
try:
# 处理图片URL
pic_data = item.get('pic', {})
pic_url = ''
if pic_data:
# 优先使用normal尺寸的图片
pic_url = pic_data.get('normal', '') or pic_data.get('large', '')
# 处理评分数据
rating_data = item.get('rating', {})
rating = None
if rating_data and rating_data.get('value'):
rating = {'value': rating_data.get('value')}
# 处理URL - 将douban://协议转换为标准HTTP链接
original_url = item.get('url', '') or item.get('uri', '')
processed_url = ''
if original_url:
if original_url.startswith('douban://douban.com/'):
# 将 douban://douban.com/movie/123 转换为 https://movie.douban.com/subject/123/
if '/movie/' in original_url:
# 提取ID部分
movie_id = original_url.split('/movie/')[-1]
processed_url = f'https://movie.douban.com/subject/{movie_id}/'
elif '/tv/' in original_url:
# 提取ID部分
tv_id = original_url.split('/tv/')[-1]
processed_url = f'https://movie.douban.com/subject/{tv_id}/'
else:
processed_url = original_url
else:
processed_url = original_url
processed = {
'id': item.get('id', ''),
'title': item.get('title', ''),
'year': item.get('year', ''),
'url': processed_url,
'pic': {
'normal': pic_url
},
'rating': rating,
'card_subtitle': item.get('card_subtitle', '')
}
# 确保必要字段存在
if not processed['title']:
return None
return processed
except Exception:
return None
def _get_mock_movie_data(self) -> Dict[str, Any]:
"""获取模拟电影数据"""
return {
'notice': "⚠️ 这是模拟数据,非豆瓣实时数据",
'items': [
{
'id': "1292052",
'title': "肖申克的救赎",
'rating': {'value': 9.7},
'year': "1994",
'url': "https://movie.douban.com/subject/1292052/",
'pic': {'normal': ""},
'card_subtitle': "1994 / 美国 / 剧情 犯罪 / 弗兰克·德拉邦特 / 蒂姆·罗宾斯 摩根·弗里曼"
},
{
'id': "1291546",
'title': "霸王别姬",
'rating': {'value': 9.6},
'year': "1993",
'url': "https://movie.douban.com/subject/1291546/",
'pic': {'normal': ""},
'card_subtitle': "1993 / 中国大陆 香港 / 剧情 爱情 同性 / 陈凯歌 / 张国荣 张丰毅"
},
{
'id': "1295644",
'title': "阿甘正传",
'rating': {'value': 9.5},
'year': "1994",
'url': "https://movie.douban.com/subject/1295644/",
'pic': {'normal': ""},
'card_subtitle': "1994 / 美国 / 剧情 爱情 / 罗伯特·泽米吉斯 / 汤姆·汉克斯 罗宾·怀特"
}
],
'total': 3
}
def _get_mock_tv_data(self) -> Dict[str, Any]:
"""获取模拟电视剧数据"""
return {
'notice': "⚠️ 这是模拟数据,非豆瓣实时数据",
'items': [
{
'id': "26794435",
'title': "请回答1988",
'rating': {'value': 9.7},
'year': "2015",
'url': "https://movie.douban.com/subject/26794435/",
'pic': {'normal': ""},
'card_subtitle': "2015 / 韩国 / 剧情 喜剧 家庭 / 申源浩 / 李惠利 朴宝剑"
},
{
'id': "1309163",
'title': "大明王朝1566",
'rating': {'value': 9.7},
'year': "2007",
'url': "https://movie.douban.com/subject/1309163/",
'pic': {'normal': ""},
'card_subtitle': "2007 / 中国大陆 / 剧情 历史 / 张黎 / 陈宝国 黄志忠"
},
{
'id': "1309169",
'title': "亮剑",
'rating': {'value': 9.3},
'year': "2005",
'url': "https://movie.douban.com/subject/1309169/",
'pic': {'normal': ""},
'card_subtitle': "2005 / 中国大陆 / 剧情 战争 / 陈健 张前 / 李幼斌 何政军"
}
],
'total': 3
}
# 创建全局实例
douban_service = DoubanService()

View File

@ -1430,20 +1430,23 @@ button.close:focus,
/* --------------- 文件选择弹窗样式 --------------- */
/* 文件选择弹窗整体样式 */
#fileSelectModal .modal-dialog {
#fileSelectModal .modal-dialog,
#createTaskModal .modal-dialog {
max-width: 1080px;
margin: 4rem auto;
width: calc(100% - 1.25rem); /* 左右各保留1.5rem的最小边距 */
}
#fileSelectModal .modal-content {
#fileSelectModal .modal-content,
#createTaskModal .modal-content {
border-radius: 6px;
border: 1px solid var(--border-color);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1);
}
/* 弹窗头部样式 */
#fileSelectModal .modal-header {
#fileSelectModal .modal-header,
#createTaskModal .modal-header {
background-color: #fff;
border-bottom: 1px solid var(--border-color);
padding: 11px 16px;
@ -1451,7 +1454,8 @@ button.close:focus,
border-top-right-radius: 6px;
}
#fileSelectModal .modal-title {
#fileSelectModal .modal-title,
#createTaskModal .modal-title {
font-size: 1.2rem;
font-weight: 500;
color: var(--dark-text-color);
@ -1459,12 +1463,14 @@ button.close:focus,
align-items: center;
}
#fileSelectModal .modal-title b {
#fileSelectModal .modal-title b,
#createTaskModal .modal-title b {
font-weight: 500;
}
/* 弹窗关闭按钮 */
#fileSelectModal .close {
#fileSelectModal .close,
#createTaskModal .close {
font-size: 1.4rem;
padding: 8px;
margin: -8px -8px -8px auto;
@ -1475,6 +1481,7 @@ button.close:focus,
/* 修改关闭按钮样式,使用 bi-x-lg 图标 */
#fileSelectModal .close .bi-x-lg,
#createTaskModal .close .bi-x-lg,
.modal .close .bi-x-lg {
font-size: 1.2rem;
color: var(--dark-text-color);
@ -1484,17 +1491,35 @@ button.close:focus,
right: -2px; /* 向左移动2px */
}
#fileSelectModal .close:hover {
#fileSelectModal .close:hover,
#createTaskModal .close:hover {
opacity: 1;
color: var(--dark-text-color);
}
/* 弹窗主体样式 */
#fileSelectModal .modal-body {
#fileSelectModal .modal-body,
#createTaskModal .modal-body {
padding: 16px;
font-size: 0.875rem;
}
/* 创建任务模态框主内容区相对定位 */
#createTaskModal .modal-body {
position: relative;
}
/* 创建任务模态框主内容区底部分割线 */
#createTaskModal .modal-body::after {
content: '';
position: absolute;
bottom: 7px;
left: 16px;
right: 16px;
height: 1px;
background-color: var(--border-color);
}
/* 弹窗内警告框样式 */
#fileSelectModal .alert-warning {
font-size: 0.85rem; /* 保留特定的字体大小 */
@ -1722,7 +1747,8 @@ button.close:focus,
}
/* 弹窗底部样式 */
#fileSelectModal .modal-footer {
#fileSelectModal .modal-footer,
#createTaskModal .modal-footer {
border-top: none; /* 隐藏底部分割线 */
padding: 0px 16px 12px 16px; /* 上 右 下 左设置上内边距为0 */
margin-top: -4px; /* 使用负margin使整个底部区域向上移动 */
@ -1733,19 +1759,22 @@ button.close:focus,
}
/* 添加文件选择模态框左下角文件信息文本的左边距样式 */
#fileSelectModal .modal-footer .file-selection-info {
#fileSelectModal .modal-footer .file-selection-info,
#createTaskModal .modal-footer .file-selection-info {
margin-left: 0px; /* 与表格左边距保持一致 */
font-size: 0.85rem !important; /* 覆盖内联样式 */
}
#fileSelectModal .modal-footer span {
#fileSelectModal .modal-footer span,
#createTaskModal .modal-footer span {
font-size: 0.85rem;
color: var(--dark-text-color);
margin-right: auto;
}
/* 弹窗底部按钮样式 */
#fileSelectModal .btn-primary {
#fileSelectModal .btn-primary,
#createTaskModal .btn-primary {
background-color: var(--focus-border-color);
border-color: var(--focus-border-color);
font-size: 0.85rem;
@ -1759,34 +1788,40 @@ button.close:focus,
}
/* 弹窗底部按钮内的标记样式 */
#fileSelectModal .btn-primary .badge {
#fileSelectModal .btn-primary .badge,
#createTaskModal .btn-primary .badge {
margin-left: 5px;
display: flex;
align-items: center;
}
#fileSelectModal .btn-primary:hover {
#fileSelectModal .btn-primary:hover,
#createTaskModal .btn-primary:hover {
background-color: #0A42CC !important;
border-color: #0A42CC !important;
}
#fileSelectModal .btn-sm {
#fileSelectModal .btn-sm,
#createTaskModal .btn-sm {
font-size: 0.85rem;
}
/* 弹窗底部有文本内容的按钮样式 */
#fileSelectModal .btn-primary:has(span) {
#fileSelectModal .btn-primary:has(span),
#createTaskModal .btn-primary:has(span) {
width: auto;
padding: 0 8px;
}
#fileSelectModal .btn-primary:not(:has(span)) {
#fileSelectModal .btn-primary:not(:has(span)),
#createTaskModal .btn-primary:not(:has(span)) {
width: auto;
min-width: 32px;
padding: 0 8px;
}
#fileSelectModal .btn-sm {
#fileSelectModal .btn-sm,
#createTaskModal .btn-sm {
font-size: 0.85rem;
}
@ -2156,6 +2191,10 @@ div.jsoneditor-tree button.jsoneditor-button:focus {
font-size: 1rem;
}
.sidebar .nav-link .bi-film {
font-size: 0.94rem;
}
.sidebar .nav-link .bi-power {
font-size: 1.27rem;
}
@ -3052,6 +3091,17 @@ div.jsoneditor-treepath * {
right: -1px; /* 向右移动右箭头 */
}
/* --------------- 系统配置页面form-group间距统一 --------------- */
/* 系统配置页面中所有form-group的间距统一为8px与其他模块保持一致 */
main .form-group.mb-2 {
margin-bottom: 8px !important;
}
/* 任务设置模块的form-group间距调整为8px */
main .form-group:not(.row) {
margin-bottom: 8px !important;
}
/* --------------- 魔法匹配输入框比例设置 --------------- */
/* 系统配置页面中魔法匹配的输入框比例 */
.form-group.mb-2 > .input-group {
@ -3310,7 +3360,8 @@ div[id^="collapse_"][id*="plugin"] .input-group {
min-height: 32px;
}
#fileSelectModal .btn-primary span {
#fileSelectModal .btn-primary span,
#createTaskModal .btn-primary span {
background-color: transparent;
color: inherit;
font-size: inherit;
@ -3319,7 +3370,8 @@ div[id^="collapse_"][id*="plugin"] .input-group {
margin: 0;
}
#fileSelectModal .btn-primary .badge-light {
#fileSelectModal .btn-primary .badge-light,
#createTaskModal .btn-primary .badge-light {
background-color: transparent;
color: inherit;
font-size: inherit;
@ -5371,6 +5423,8 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
border-color: var(--button-gray-background-color) !important;
color: var(--dark-text-color) !important;
}
#fileSelectModal[data-modal-type="preview-filemanager"] .modal-footer .btn-cancel:hover {
background-color: #e0e2e6 !important;
border-color: #e0e2e6 !important;
@ -5389,6 +5443,355 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
color: var(--dark-text-color) !important;
}
/* 创建任务模态框的取消按钮样式 */
#createTaskModal .modal-footer .btn-cancel {
background-color: var(--button-gray-background-color) !important;
border-color: var(--button-gray-background-color) !important;
color: var(--dark-text-color) !important;
}
#createTaskModal .modal-footer .btn-cancel:hover {
background-color: #e0e2e6 !important;
border-color: #e0e2e6 !important;
color: var(--dark-text-color) !important;
}
/* --------------- 模态框层级管理 --------------- */
/* 当从创建任务模态框中打开文件选择模态框时,确保文件选择模态框显示在上层 */
#createTaskModal.show ~ #fileSelectModal {
z-index: 1060 !important;
}
#createTaskModal.show ~ #fileSelectModal .modal-backdrop {
z-index: 1055 !important;
}
/* --------------- 创建任务模态框使用任务列表样式 --------------- */
/* 创建任务模态框底部间距调整 */
#createTaskModal .modal-footer {
margin-top: 5px; /* 调整为5px让分割线距离按钮16px */
}
/* 创建任务模态框中的搜索按钮样式 */
#createTaskModal .btn-primary:has(.bi-search) {
background-color: transparent;
border-color: var(--dark-text-color);
color: var(--dark-text-color);
width: 32px;
height: 32px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0; /* 去掉圆角 */
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
#createTaskModal .btn-primary:has(.bi-search):hover {
background-color: var(--dark-text-color) !important; /* 使用!important确保优先级 */
border-color: var(--dark-text-color) !important;
color: #fff !important;
}
#createTaskModal .btn-primary:hover .bi-search {
color: #fff !important;
}
/* 覆盖可能存在的其他btn-primary样式 */
#createTaskModal .input-group-append .btn-primary:has(.bi-search):hover {
background-color: var(--dark-text-color) !important;
border-color: var(--dark-text-color) !important;
color: #fff !important;
}
#createTaskModal .input-group-append .btn-primary:has(.bi-search) {
background-color: transparent !important;
border-color: var(--dark-text-color) !important;
color: var(--dark-text-color) !important;
}
/* 创建任务模态框中的输入组按钮样式 */
#createTaskModal .input-group-append .btn,
#createTaskModal .input-group-prepend .btn {
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
#createTaskModal .input-group-append .btn:has(i.bi):not(.btn-primary),
#createTaskModal .input-group-prepend .btn:has(i.bi):not(.btn-primary) {
width: 32px;
height: 32px;
padding: 0;
}
/* 创建任务模态框中的图标悬停样式 */
#createTaskModal .btn-outline-secondary:hover .bi-folder,
#createTaskModal .btn-outline-secondary:hover .bi-calendar3,
#createTaskModal .btn-outline-secondary:hover .bi-reply,
#createTaskModal .btn-outline-secondary:hover .bi-folder-x,
#createTaskModal .input-group-text:hover .bi-google,
#createTaskModal .input-group-text:hover .bi-link-45deg,
#createTaskModal .input-group-text:hover .tmdb-icon,
#createTaskModal .input-group-text:hover .douban-icon {
color: #fff;
}
/* 创建任务模态框中的input-group-text样式 */
#createTaskModal .input-group-text {
background-color: #fff;
border-color: var(--border-color);
color: var(--dark-text-color);
height: 32px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px;
transition: all 0.2s ease;
}
#createTaskModal .input-group-text:hover {
background-color: var(--dark-text-color);
border-color: var(--dark-text-color);
color: #fff;
}
/* 忽略后缀文本不应该有悬停效果 */
#createTaskModal .input-group-text:has(input[type="checkbox"]) {
background-color: var(--button-gray-background-color) !important;
border-color: var(--border-color) !important;
color: var(--dark-text-color) !important;
}
#createTaskModal .input-group-text:has(input[type="checkbox"]):hover {
background-color: var(--button-gray-background-color) !important;
border-color: var(--border-color) !important;
color: var(--dark-text-color) !important;
}
/* 创建任务模态框中的按钮样式 - 完全复制任务列表样式 */
#createTaskModal .btn-outline-secondary:hover {
background-color: var(--dark-text-color) !important;
border-color: var(--dark-text-color) !important;
color: #fff !important;
}
#createTaskModal .btn-outline-secondary:active {
background-color: var(--dark-text-color) !important;
border-color: var(--dark-text-color) !important;
color: #fff !important;
}
/* 修复创建任务模态框中日历按钮点击后背景色不恢复的问题 */
#createTaskModal .btn-outline-secondary:has(.bi-calendar3):focus,
#createTaskModal .btn-outline-secondary:has(.bi-calendar3):active,
#createTaskModal .btn-outline-secondary:has(.bi-calendar3).focus,
#createTaskModal .btn-outline-secondary:has(.bi-calendar3).active {
background-color: transparent !important;
border-color: var(--dark-text-color) !important;
color: var(--dark-text-color) !important;
box-shadow: none !important;
outline: none !important;
}
/* 强制覆盖所有可能的日历按钮状态 */
#createTaskModal .input-group-append .btn-outline-secondary:has(.bi-calendar3):focus,
#createTaskModal .input-group-append .btn-outline-secondary:has(.bi-calendar3):active,
#createTaskModal .input-group-append .btn-outline-secondary:has(.bi-calendar3).focus,
#createTaskModal .input-group-append .btn-outline-secondary:has(.bi-calendar3).active {
background-color: transparent !important;
border-color: var(--dark-text-color) !important;
color: var(--dark-text-color) !important;
box-shadow: none !important;
outline: none !important;
}
/* 确保日历按钮悬停时正确显示 */
#createTaskModal .btn-outline-secondary:has(.bi-calendar3):hover,
#createTaskModal .input-group-append .btn-outline-secondary:has(.bi-calendar3):hover {
background-color: var(--dark-text-color) !important;
border-color: var(--dark-text-color) !important;
color: #fff !important;
}
/* 创建任务模态框中的图标样式 */
#createTaskModal .tmdb-icon,
#createTaskModal .douban-icon {
width: 16px;
height: 16px;
transition: filter 0.2s ease;
}
#createTaskModal .input-group-text:hover .tmdb-icon,
#createTaskModal .input-group-text:hover .douban-icon {
filter: brightness(0) invert(1); /* 将图标变为白色 */
}
/* 创建任务模态框中的输入组焦点样式 */
#createTaskModal .input-group .form-control:focus + .input-group-append .btn,
#createTaskModal .input-group .form-control:focus + .input-group-append .btn:focus {
border-left-color: #2563eb !important;
box-shadow: none !important;
position: relative;
z-index: 2;
}
/* 创建任务模态框中的表单组样式 */
#createTaskModal .form-group.row {
margin-bottom: 8px; /* 设置行之间的间距为8px */
padding-top: 0;
padding-bottom: 0;
}
/* 创建任务模态框中的标签样式 */
#createTaskModal .form-group.row .col-sm-2 {
max-width: 104px; /* 设置最大宽度为104px */
min-width: 104px; /* 保持最小宽度一致 */
width: 104px; /* 固定宽度 */
}
/* 创建任务模态框中的输入框列自适应 */
#createTaskModal .form-group.row .col-sm-10 {
width: calc(100% - 104px); /* 计算剩余宽度 */
max-width: calc(100% - 104px); /* 最大宽度也应该计算 */
flex: 1; /* 允许伸缩 */
}
/* 创建任务模态框中的标签样式 */
#createTaskModal .form-group.row .col-form-label {
padding-top: 4px;
padding-bottom: 4px;
font-size: 0.95rem; /* 与输入框字体大小一致 */
font-weight: normal; /* 标准字重 */
line-height: 1.5; /* 与输入框行高一致 */
}
/* 创建任务模态框中的表单控件样式 */
#createTaskModal .form-group.row .form-control,
#createTaskModal .form-group.row .input-group {
margin-bottom: 0;
}
/* 创建任务模态框中的任务建议样式 */
#createTaskModal .task-suggestions {
width: 100%;
max-height: 482px;
overflow-y: auto;
transform: translate(0, 0);
top: 100%;
}
#createTaskModal .task-suggestions .dropdown-item {
padding: 0 8px;
margin: 0;
height: 30px;
line-height: 30px;
box-sizing: border-box;
}
#createTaskModal .task-suggestions .dropdown-item:hover {
background-color: var(--button-gray-background-color);
color: var(--dark-text-color);
}
#createTaskModal .task-suggestions .dropdown-item:active {
background-color: var(--focus-border-color) !important;
color: #fff !important;
}
#createTaskModal .task-suggestions .dropdown-item small a {
color: var(--light-text-color);
text-decoration: none;
transition: color 0.2s;
font-size: 14px;
}
#createTaskModal .task-suggestions .dropdown-item:hover small a {
color: var(--dark-text-color);
}
#createTaskModal .task-suggestions .dropdown-item:active small a {
color: #fff !important;
}
#createTaskModal .task-suggestions .dropdown-item.text-muted {
height: 30px;
line-height: 30px;
padding: 0 8px !important;
font-size: 14px !important;
box-sizing: border-box;
}
#createTaskModal .task-suggestions .dropdown-item.text-muted:hover {
color: var(--dark-text-color) !important;
}
#createTaskModal .task-suggestions .dropdown-item.text-muted:not(.text-center) {
font-size: 14px !important;
text-align: left !important;
padding-left: 8px !important;
color: var(--light-text-color) !important;
}
#createTaskModal .task-suggestions .dropdown-item.text-muted:active {
color: #fff !important;
}
/* 创建任务模态框移动端响应式样式 */
@media (max-width: 767.98px) {
#createTaskModal .form-group.row .col-sm-2 {
max-width: 100%; /* 在小屏上允许全宽 */
min-width: auto; /* 取消最小宽度限制 */
width: 100%; /* 宽度适应屏幕 */
padding-top: 0; /* 移除顶部内边距 */
padding-bottom: 2px; /* 减少底部内边距 */
}
#createTaskModal .form-group.row .col-sm-10 {
width: 100%; /* 全宽 */
max-width: 100%; /* 最大宽度全宽 */
margin-top: 0px; /* 减少顶部间距 */
padding-top: 0; /* 移除顶部内边距 */
padding-bottom: 0; /* 移除底部内边距 */
}
/* 确保移动模式下配置选项间距与桌面模式一致 */
#createTaskModal .form-group.row {
margin-bottom: 8px; /* 与桌面模式保持一致的行间距 */
padding-top: 0; /* 移除顶部内边距 */
padding-bottom: 0; /* 移除底部内边距 */
}
/* 调整移动模式下表单控件的间距 */
#createTaskModal .form-group.row .form-control,
#createTaskModal .form-group.row .input-group {
margin-bottom: 0; /* 确保无底部边距 */
}
/* 调整任务配置标签的间距 */
#createTaskModal .form-group.row .col-form-label {
padding-top: 2px; /* 减少顶部内边距 */
padding-bottom: 2px; /* 减少底部内边距 */
}
/* 专门针对配置选项标题在配置框上方的情况调整 */
#createTaskModal .form-group.row:not(.align-items-center) .col-sm-2 {
margin-bottom: 4.5px; /* 设置标题与配置框之间的距离 */
padding-left: 15px; /* 左对齐与其他元素保持一致 */
font-size: 0.95rem; /* 保持字体大小一致 */
}
/* 标题在上方时配置框的左内边距调整 */
#createTaskModal .form-group.row:not(.align-items-center) .col-sm-10 {
padding-left: 15px; /* 为标题在上方的配置框增加左内边距 */
}
}
@media (max-width: 767.98px) {
.file-manager-rule-bar-responsive {
display: flex;
@ -5496,3 +5899,264 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
.file-manager-rule-bar { display: flex !important; }
.file-manager-rule-bar-responsive { display: none !important; }
}
/* --------------- 影视发现页面样式 --------------- */
.discovery-controls {
margin-bottom: 20px;
}
.discovery-main-buttons {
margin-bottom: 8px;
display: flex;
flex-wrap: nowrap;
gap: 8px;
overflow-x: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.discovery-main-buttons::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
.discovery-sub-buttons {
margin-bottom: 20px;
display: flex;
flex-wrap: nowrap;
gap: 8px;
overflow-x: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.discovery-sub-buttons::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
.discovery-btn {
border-radius: 6px;
font-size: 0.95rem;
padding: 0 8px;
height: 32px;
border: 1px solid var(--dark-text-color);
background-color: transparent;
color: var(--dark-text-color);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
flex-shrink: 0;
min-width: fit-content;
}
.discovery-btn:hover {
background-color: var(--dark-text-color);
border-color: var(--dark-text-color);
color: white;
}
.discovery-btn.active {
background-color: var(--focus-border-color) !important;
border-color: var(--focus-border-color) !important;
color: white !important;
}
.discovery-btn:focus {
box-shadow: none;
outline: none;
}
.discovery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 20px;
padding: 0;
}
.discovery-poster {
position: relative;
width: 100%;
aspect-ratio: 2/3; /* 强制保持2:3比例 */
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 10px;
}
.discovery-poster img {
width: 100%;
height: 100%;
object-fit: cover;
transition: filter 0.3s ease;
}
.discovery-poster:hover img {
opacity: 0;
transition: opacity 0.3s ease;
}
.discovery-poster-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 8px 5px 8px;
font-size: 0.75rem;
font-weight: normal;
line-height: 1.4;
opacity: 0;
transition: opacity 0.3s ease;
display: flex;
flex-direction: column;
justify-content: flex-end;
pointer-events: none;
}
.discovery-poster:hover .discovery-poster-overlay {
opacity: 1;
}
.discovery-poster-overlay .info-line {
margin-bottom: 5.5px;
padding-bottom: 5px;
word-wrap: break-word;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.discovery-poster-overlay .info-line:first-child {
margin-top: 0;
}
.discovery-poster-overlay .info-line:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.discovery-rating {
position: absolute;
top: 8px;
right: 8px;
background-color: rgba(0, 0, 0, 0.8);
color: #efb30a;
padding: 2px 6px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: bold;
z-index: 10;
}
.discovery-create-task {
position: absolute;
top: 8px;
left: 8px;
width: 22px;
height: 22px;
background-color: transparent;
border: 1px solid white;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: all 0.3s ease;
z-index: 10;
color: white;
font-size: 1rem;
font-weight: 210;
}
.discovery-create-task .plus-text {
transform: translateX(-0.5px) translateY(-1.5px);
}
.discovery-poster:hover .discovery-create-task {
opacity: 1;
}
.discovery-create-task:hover {
background-color: var(--focus-border-color);
border-color: var(--focus-border-color);
color: white;
}
.discovery-info {
padding: 0 0px;
text-align: left;
}
.discovery-title {
font-size: 0.95rem;
font-weight: 500;
color: var(--dark-text-color);
margin-bottom: 0px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: color 0.2s ease;
}
.discovery-title:hover {
color: var(--focus-border-color);
}
.discovery-genre {
font-size: 0.95rem;
color: #888;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.genre-slash {
position: relative;
top: -1px;
}
.discovery-year {
font-size: 0.75rem;
color: #666;
}
/* 响应式设计 */
@media (max-width: 576px) {
.discovery-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
}
.discovery-btn {
font-size: 0.95rem;
padding: 0 6px;
height: 28px;
}
.discovery-controls {
margin-bottom: 15px;
}
.discovery-main-buttons {
margin-bottom: 8px;
gap: 8px;
}
.discovery-sub-buttons {
margin-bottom: 20px;
gap: 8px;
}
}
@media (min-width: 1200px) {
.discovery-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
}

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 503 B

After

Width:  |  Height:  |  Size: 503 B

View File

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 553 B

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@ -0,0 +1,7 @@
<svg width="150" height="225" viewBox="0 0 150 225" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="150" height="225" fill="#F5F5F5"/>
<rect x="50" y="80" width="50" height="65" rx="4" fill="#CCCCCC"/>
<circle cx="65" cy="100" r="8" fill="#F5F5F5"/>
<circle cx="85" cy="100" r="8" fill="#F5F5F5"/>
<rect x="60" y="115" width="30" height="20" rx="2" fill="#F5F5F5"/>
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@ -166,10 +166,38 @@ function sortFileByName(file) {
}
}
// 3. 上中下
if (/[上][集期话部篇]?|[集期话部篇]上/.test(filename)) segment_value = 1;
else if (/[中][集期话部篇]?|[集期话部篇]中/.test(filename)) segment_value = 2;
else if (/[下][集期话部篇]?|[集期话部篇]下/.test(filename)) segment_value = 3;
// 3. 上中下标记或其他细分 - 第三级排序键
let segment_base = 0; // 基础值:上=1, 中=2, 下=3
let sequence_number = 0; // 序号值:用于处理上中下后的数字或中文数字序号
if (/[上][集期话部篇]?|[集期话部篇]上/.test(filename)) {
segment_base = 1;
} else if (/[中][集期话部篇]?|[集期话部篇]中/.test(filename)) {
segment_base = 2;
} else if (/[下][集期话部篇]?|[集期话部篇]下/.test(filename)) {
segment_base = 3;
}
// 当有上中下标记时,进一步提取后续的序号
if (segment_base > 0) {
// 提取上中下后的中文数字序号,如:上(一)、上(二)
let chinese_seq_match = filename.match(/[上中下][集期话部篇]?[(]([一二三四五六七八九十百千万零两]+)[)]/);
if (chinese_seq_match) {
let arabic_num = chineseToArabic(chinese_seq_match[1]);
if (arabic_num !== null) {
sequence_number = arabic_num;
}
} else {
// 提取上中下后的阿拉伯数字序号上1、上2
let arabic_seq_match = filename.match(/[上中下][集期话部篇]?(\d+)/);
if (arabic_seq_match) {
sequence_number = parseInt(arabic_seq_match[1]);
}
}
}
// 组合segment_value基础值*1000 + 序号值,确保排序正确
segment_value = segment_base * 1000 + sequence_number;
return [date_value, episode_value, segment_value, update_time, pinyin_sort_key];
}

File diff suppressed because it is too large Load Diff

View File

@ -227,12 +227,33 @@ def sort_file_by_name(file):
episode_value = int(any_num_match.group(1))
# 3. 提取上中下标记或其他细分 - 第三级排序键
segment_base = 0 # 基础值:上=1, 中=2, 下=3
sequence_number = 0 # 序号值:用于处理上中下后的数字或中文数字序号
if re.search(r'上[集期话部篇]?|[集期话部篇]上', filename):
segment_value = 1
segment_base = 1
elif re.search(r'中[集期话部篇]?|[集期话部篇]中', filename):
segment_value = 2
segment_base = 2
elif re.search(r'下[集期话部篇]?|[集期话部篇]下', filename):
segment_value = 3
segment_base = 3
# 当有上中下标记时,进一步提取后续的序号
if segment_base > 0:
# 提取上中下后的中文数字序号,如:上(一)、上(二)
chinese_seq_match = re.search(r'[上中下][集期话部篇]?[(]([一二三四五六七八九十百千万零两]+)[)]', filename)
if chinese_seq_match:
chinese_num = chinese_seq_match.group(1)
arabic_num = chinese_to_arabic(chinese_num)
if arabic_num is not None:
sequence_number = arabic_num
else:
# 提取上中下后的阿拉伯数字序号上1、上2
arabic_seq_match = re.search(r'[上中下][集期话部篇]?(\d+)', filename)
if arabic_seq_match:
sequence_number = int(arabic_seq_match.group(1))
# 组合segment_value基础值*1000 + 序号值,确保排序正确
segment_value = segment_base * 1000 + sequence_number
# 返回多级排序元组,加入更新时间作为第四级排序键,拼音排序作为第五级排序键
return (date_value, episode_value, segment_value, update_time, pinyin_sort_key)
@ -337,14 +358,14 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
if match_e:
return int(match_e.group(1))
# 尝试匹配更多格式
# 尝试匹配更多格式(注意:避免匹配季数)
default_patterns = [
r'第(\d+)集',
r'第(\d+)期',
r'第(\d+)话',
r'(\d+)集',
r'(\d+)期',
r'(\d+)话',
r'(?<!第\d+季\s*)(\d+)集', # 避免匹配"第X季 Y集"中的季数
r'(?<!第\d+季\s*)(\d+)期', # 避免匹配"第X季 Y期"中的季数
r'(?<!第\d+季\s*)(\d+)话', # 避免匹配"第X季 Y话"中的季数
r'[Ee][Pp]?(\d+)',
r'(\d+)[-_\s]*4[Kk]',
r'\[(\d+)\]',
@ -379,16 +400,67 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None):
# 尝试使用每个正则表达式匹配文件名(使用不含日期的文件名)
for pattern_regex in patterns:
try:
match = re.search(pattern_regex, filename_without_dates)
if match:
episode_num = int(match.group(1))
# 特殊处理:如果是包含多个捕获组的复合正则表达式
if '|' in pattern_regex and '(' in pattern_regex:
# 先尝试匹配集/期/话相关的模式,避免误匹配季数
episode_specific_patterns = [
r'第(\d+)集', r'第(\d+)期', r'第(\d+)话',
r'(\d+)集', r'(\d+)期', r'(\d+)话'
]
# 检查提取的数字是否可能是日期的一部分
# 如果是纯数字并且可能是日期格式,则跳过
if str(episode_num).isdigit() and is_date_format(str(episode_num)):
continue
for ep_pattern in episode_specific_patterns:
ep_match = re.search(ep_pattern, filename_without_dates)
if ep_match:
# 检查这个匹配是否紧跟在"第X季"后面,如果是则跳过
match_start = ep_match.start()
prefix = filename_without_dates[:match_start]
if re.search(r'\d+季\s*$', prefix):
continue # 跳过紧跟在季数后的匹配
return episode_num
episode_num = int(ep_match.group(1))
# 检查提取的数字是否可能是日期的一部分
if str(episode_num).isdigit() and is_date_format(str(episode_num)):
continue
return episode_num
# 如果集/期/话模式都没匹配到,再尝试原始的复合正则表达式
match = re.search(pattern_regex, filename_without_dates)
if match:
# 遍历所有捕获组,找到第一个非空的
for group_num in range(1, len(match.groups()) + 1):
if match.group(group_num):
episode_num = int(match.group(group_num))
# 检查提取的数字是否可能是日期的一部分
if str(episode_num).isdigit() and is_date_format(str(episode_num)):
continue
# 额外检查:如果匹配的数字来自"第X季"格式,跳过
match_start = match.start()
match_end = match.end()
# 检查匹配前后的上下文,看是否是"第X季"格式
context_start = max(0, match_start - 2)
context_end = min(len(filename_without_dates), match_end + 1)
context = filename_without_dates[context_start:context_end]
if re.search(r'\d+季', context):
continue
return episode_num
else:
# 单一模式的正则表达式
match = re.search(pattern_regex, filename_without_dates)
if match:
episode_num = int(match.group(1))
# 检查提取的数字是否可能是日期的一部分
if str(episode_num).isdigit() and is_date_format(str(episode_num)):
continue
return episode_num
except:
continue