新增影视发现功能
514
app/douban_service.py
Normal 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()
|
||||
106
app/run.py
@ -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 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()
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 503 B |
|
Before Width: | Height: | Size: 553 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
7
app/static/images/no-poster.svg
Normal 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 |