From 2fbfd393c19ba5efee361bd303359b65fe81935c Mon Sep 17 00:00:00 2001 From: x1ao4 Date: Sun, 13 Jul 2025 23:03:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BD=B1=E8=A7=86=E5=8F=91?= =?UTF-8?q?=E7=8E=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/douban_service.py | 514 ++++++++ app/run.py | 106 +- app/static/css/main.css | 704 ++++++++++- app/static/{ => images}/Douban.svg | 0 app/static/{ => images}/Plex.svg | 0 app/static/{ => images}/TMDB.svg | 0 app/static/{ => images}/favicon.ico | Bin app/static/images/no-poster.svg | 7 + app/templates/index.html | 1758 +++++++++++++++++++++++++-- 9 files changed, 2997 insertions(+), 92 deletions(-) create mode 100644 app/douban_service.py rename app/static/{ => images}/Douban.svg (100%) rename app/static/{ => images}/Plex.svg (100%) rename app/static/{ => images}/TMDB.svg (100%) rename app/static/{ => images}/favicon.ico (100%) create mode 100644 app/static/images/no-poster.svg diff --git a/app/douban_service.py b/app/douban_service.py new file mode 100644 index 0000000..c910e42 --- /dev/null +++ b/app/douban_service.py @@ -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() diff --git a/app/run.py b/app/run.py index cf99055..c91c206 100644 --- a/app/run.py +++ b/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//") +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//") +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() diff --git a/app/static/css/main.css b/app/static/css/main.css index 2666e50..446b1ca 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -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)); + } +} diff --git a/app/static/Douban.svg b/app/static/images/Douban.svg similarity index 100% rename from app/static/Douban.svg rename to app/static/images/Douban.svg diff --git a/app/static/Plex.svg b/app/static/images/Plex.svg similarity index 100% rename from app/static/Plex.svg rename to app/static/images/Plex.svg diff --git a/app/static/TMDB.svg b/app/static/images/TMDB.svg similarity index 100% rename from app/static/TMDB.svg rename to app/static/images/TMDB.svg diff --git a/app/static/favicon.ico b/app/static/images/favicon.ico similarity index 100% rename from app/static/favicon.ico rename to app/static/images/favicon.ico diff --git a/app/static/images/no-poster.svg b/app/static/images/no-poster.svg new file mode 100644 index 0000000..29a1418 --- /dev/null +++ b/app/static/images/no-poster.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/templates/index.html b/app/templates/index.html index fa2bde8..11d00a1 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -374,6 +374,11 @@ 文件整理 +