Merge pull request #74 from x1ao4/dev

新增运行日志、播出时间相关功能及其他优化
This commit is contained in:
x1ao4 2025-12-24 23:02:17 +08:00 committed by GitHub
commit ce8659bb01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 4816 additions and 513 deletions

2178
app/run.py

File diff suppressed because it is too large Load Diff

View File

@ -361,15 +361,37 @@ class CalendarDB:
) )
''') ''')
# 检查 content_type 字段是否存在,如果不存在则添加 # 检查 shows 表的新增字段(兼容旧版本)
cursor.execute("PRAGMA table_info(shows)") cursor.execute("PRAGMA table_info(shows)")
columns = [column[1] for column in cursor.fetchall()] columns = [column[1] for column in cursor.fetchall()]
# 内容类型
if 'content_type' not in columns: if 'content_type' not in columns:
cursor.execute('ALTER TABLE shows ADD COLUMN content_type TEXT') cursor.execute('ALTER TABLE shows ADD COLUMN content_type TEXT')
columns.append('content_type')
# 检查 is_custom_poster 字段是否存在,如果不存在则添加 # 自定义海报标记
if 'is_custom_poster' not in columns: if 'is_custom_poster' not in columns:
cursor.execute('ALTER TABLE shows ADD COLUMN is_custom_poster INTEGER DEFAULT 0') cursor.execute('ALTER TABLE shows ADD COLUMN is_custom_poster INTEGER DEFAULT 0')
columns.append('is_custom_poster')
# 本地播出时间Trakt + 时区转换后的节目级播出时间,格式 HH:MM
if 'local_air_time' not in columns:
cursor.execute('ALTER TABLE shows ADD COLUMN local_air_time TEXT')
columns.append('local_air_time')
# 节目级原始播出时间Trakt airs.time源时区下的 HH:MM
if 'air_time_source' not in columns:
cursor.execute('ALTER TABLE shows ADD COLUMN air_time_source TEXT')
columns.append('air_time_source')
# 节目级播出地时区Trakt airs.timezone例如 America/New_York
if 'air_timezone' not in columns:
cursor.execute('ALTER TABLE shows ADD COLUMN air_timezone TEXT')
columns.append('air_timezone')
# 日期偏移air_date 转换为 air_date_local 时的日期偏移天数,+1表示延后一天-1表示提前一天0表示无偏移
if 'air_date_offset' not in columns:
cursor.execute('ALTER TABLE shows ADD COLUMN air_date_offset INTEGER DEFAULT 0')
columns.append('air_date_offset')
# 标记 air_date_offset 是否由用户手动设置(通过 WebUI 编辑元数据页面)
if 'air_date_offset_manually_set' not in columns:
cursor.execute('ALTER TABLE shows ADD COLUMN air_date_offset_manually_set INTEGER DEFAULT 0')
columns.append('air_date_offset_manually_set')
# seasons # seasons
cursor.execute(''' cursor.execute('''
@ -400,6 +422,21 @@ class CalendarDB:
UNIQUE (tmdb_id, season_number, episode_number) UNIQUE (tmdb_id, season_number, episode_number)
) )
''') ''')
# 迁移:为 episodes 表新增 Trakt 播出时间相关字段(兼容旧版本)
try:
cursor.execute('PRAGMA table_info(episodes)')
ep_columns = [column[1] for column in cursor.fetchall()]
if 'air_datetime_utc' not in ep_columns:
cursor.execute('ALTER TABLE episodes ADD COLUMN air_datetime_utc TEXT')
ep_columns.append('air_datetime_utc')
if 'air_datetime_local' not in ep_columns:
cursor.execute('ALTER TABLE episodes ADD COLUMN air_datetime_local TEXT')
ep_columns.append('air_datetime_local')
if 'air_date_local' not in ep_columns:
cursor.execute('ALTER TABLE episodes ADD COLUMN air_date_local TEXT')
except Exception:
# 迁移失败不影响主流程,后续逻辑会根据列是否存在做兼容处理
pass
# season_metrics缓存每季的三项计数 # season_metrics缓存每季的三项计数
cursor.execute(''' cursor.execute('''
@ -599,11 +636,22 @@ class CalendarDB:
''', (tmdb_id, season_number, episode_number, name, overview, air_date, runtime, ep_type, updated_at)) ''', (tmdb_id, season_number, episode_number, name, overview, air_date, runtime, ep_type, updated_at))
self.conn.commit() self.conn.commit()
@retry_on_locked(max_retries=3, base_delay=0.1)
def update_episode_air_date_local(self, tmdb_id: int, season_number: int, episode_number: int, air_date_local: str):
"""更新单集的本地播出日期"""
cursor = self.conn.cursor()
cursor.execute('''
UPDATE episodes
SET air_date_local = ?
WHERE tmdb_id = ? AND season_number = ? AND episode_number = ?
''', (air_date_local, tmdb_id, season_number, episode_number))
self.conn.commit()
@retry_on_locked(max_retries=3, base_delay=0.1) @retry_on_locked(max_retries=3, base_delay=0.1)
def list_latest_season_episodes(self, tmdb_id:int, latest_season:int): def list_latest_season_episodes(self, tmdb_id:int, latest_season:int):
cursor = self.conn.cursor() cursor = self.conn.cursor()
cursor.execute(''' cursor.execute('''
SELECT episode_number, name, overview, air_date, runtime, type SELECT episode_number, name, overview, air_date, runtime, type, air_date_local
FROM episodes FROM episodes
WHERE tmdb_id=? AND season_number=? WHERE tmdb_id=? AND season_number=?
ORDER BY episode_number ASC ORDER BY episode_number ASC
@ -617,6 +665,7 @@ class CalendarDB:
'air_date': r[3], 'air_date': r[3],
'runtime': r[4], 'runtime': r[4],
'type': r[5], 'type': r[5],
'air_date_local': r[6] if len(r) > 6 else None,
} for r in rows } for r in rows
] ]
@ -642,7 +691,7 @@ class CalendarDB:
result.append(item) result.append(item)
return result return result
# --------- 孤儿数据清理seasons / episodes / season_metrics / task_metrics --------- # --------- 孤儿数据清理seasons / episodes / season_metrics / task_metrics / shows ---------
@retry_on_locked(max_retries=3, base_delay=0.1) @retry_on_locked(max_retries=3, base_delay=0.1)
def cleanup_orphan_data(self, valid_task_pairs, valid_task_names): def cleanup_orphan_data(self, valid_task_pairs, valid_task_names):
"""清理不再与任何任务对应的数据 """清理不再与任何任务对应的数据
@ -655,6 +704,7 @@ class CalendarDB:
- task_metrics: 删除 task_name 不在当前任务列表中的记录 - task_metrics: 删除 task_name 不在当前任务列表中的记录
- seasons/episodes: 仅保留出现在 valid_task_pairs 内的季与对应所有集其余删除 - seasons/episodes: 仅保留出现在 valid_task_pairs 内的季与对应所有集其余删除
- season_metrics: 仅保留出现在 valid_task_pairs 内的记录其余删除 - season_metrics: 仅保留出现在 valid_task_pairs 内的记录其余删除
- shows: 仅保留出现在 valid_task_pairs 内的 tmdb_id其余删除连带删除对应的 seasons/episodes
""" """
try: try:
cursor = self.conn.cursor() cursor = self.conn.cursor()
@ -719,6 +769,47 @@ class CalendarDB:
except Exception: except Exception:
pass pass
# 3) 清理孤立的 shows仅保留出现在 valid_task_pairs 中的 tmdb_id
# 从 valid_task_pairs 中提取所有有效的 tmdb_id
valid_tmdb_ids = set()
for tid, sn in pairs:
if tid:
valid_tmdb_ids.add(int(tid))
if not valid_tmdb_ids:
# 没有任何有效的 tmdb_id清空所有 shows
# 注意episodes 和 seasons 已经在步骤 2 中被清理了
try:
cursor.execute('DELETE FROM shows')
except Exception:
pass
else:
# 删除不在有效 tmdb_id 列表中的 shows
# 注意:对应的 episodes 和 seasons 在步骤 2 中应该已经被清理了
# 但为了确保没有残留数据,我们再次清理可能残留的孤立数据
try:
placeholders = ','.join(['?'] * len(valid_tmdb_ids))
# 先清理可能残留的孤立 episodes、seasons 和 season_metrics针对被删除的 shows
cursor.execute(
f'DELETE FROM episodes WHERE tmdb_id NOT IN ({placeholders})',
list(valid_tmdb_ids)
)
cursor.execute(
f'DELETE FROM seasons WHERE tmdb_id NOT IN ({placeholders})',
list(valid_tmdb_ids)
)
cursor.execute(
f'DELETE FROM season_metrics WHERE tmdb_id NOT IN ({placeholders})',
list(valid_tmdb_ids)
)
# 最后删除孤立的 shows
cursor.execute(
f'DELETE FROM shows WHERE tmdb_id NOT IN ({placeholders})',
list(valid_tmdb_ids)
)
except Exception:
pass
self.conn.commit() self.conn.commit()
return True return True
except Exception: except Exception:
@ -742,6 +833,214 @@ class CalendarDB:
row = cursor.fetchone() row = cursor.fetchone()
return row[0] if row else None return row[0] if row else None
# 节目本地播出时间管理(基于 Trakt 节目级 aired_time + 时区转换)
@retry_on_locked(max_retries=3, base_delay=0.1)
def update_show_local_air_time(self, tmdb_id: int, local_air_time: str):
"""
更新节目在本地时区的统一播出时间格式HH:MM
若传入空字符串或 None则清空该字段回退到全局播出集数刷新时间
"""
cursor = self.conn.cursor()
value = (local_air_time or '').strip()
cursor.execute('UPDATE shows SET local_air_time=? WHERE tmdb_id=?', (value, tmdb_id))
self.conn.commit()
return cursor.rowcount > 0
@retry_on_locked(max_retries=3, base_delay=0.1)
def get_show_local_air_time(self, tmdb_id: int):
"""获取节目在本地时区的统一播出时间HH:MM无则返回 None。"""
cursor = self.conn.cursor()
try:
cursor.execute('SELECT local_air_time FROM shows WHERE tmdb_id=?', (tmdb_id,))
except Exception:
# 旧版本可能没有该字段,直接返回 None
return None
row = cursor.fetchone()
if not row:
return None
value = row[0]
if value is None:
return None
value = str(value).strip()
return value or None
@retry_on_locked(max_retries=3, base_delay=0.1)
def update_show_air_schedule(self, tmdb_id: int, local_air_time=None, air_time_source=None, air_timezone=None, air_date_offset=None, air_date_offset_manually_set=None):
"""
批量更新节目的播出时间相关字段
- local_air_time: 本地时区统一播出时间HH:MM
- air_time_source: 源时区播出时间Trakt airs.timeHH:MM
- air_timezone: 源时区名称Trakt airs.timezone例如 America/New_York
- air_date_offset: 日期偏移天数+1表示延后一天-1表示提前一天0表示无偏移
- air_date_offset_manually_set: 标记 air_date_offset 是否由用户手动设置1表示手动设置0表示自动计算
传入 None 表示不更新该字段传入空字符串表示清空
"""
fields = []
params = []
if local_air_time is not None:
fields.append("local_air_time=?")
params.append((local_air_time or "").strip())
if air_time_source is not None:
fields.append("air_time_source=?")
params.append((air_time_source or "").strip())
if air_timezone is not None:
fields.append("air_timezone=?")
params.append((air_timezone or "").strip())
if air_date_offset is not None:
fields.append("air_date_offset=?")
params.append(int(air_date_offset) if air_date_offset is not None else 0)
if air_date_offset_manually_set is not None:
fields.append("air_date_offset_manually_set=?")
params.append(1 if air_date_offset_manually_set else 0)
if not fields:
return False
cursor = self.conn.cursor()
params.append(tmdb_id)
sql = f'UPDATE shows SET {", ".join(fields)} WHERE tmdb_id=?'
cursor.execute(sql, params)
rowcount = cursor.rowcount
self.conn.commit()
return rowcount > 0
@retry_on_locked(max_retries=3, base_delay=0.1)
def get_show_air_schedule(self, tmdb_id: int):
"""
获取节目播出时间相关配置
- local_air_time: 本地统一播出时间HH:MM
- air_time_source: 源时区播出时间HH:MM
- air_timezone: 源时区名称
- air_date_offset: 日期偏移天数+1表示延后一天-1表示提前一天0表示无偏移
如果发现没有 air_date_offset 但有时区信息会自动从已有集数据计算并补上偏移值
"""
cursor = self.conn.cursor()
try:
cursor.execute(
'SELECT local_air_time, air_time_source, air_timezone, air_date_offset, air_date_offset_manually_set FROM shows WHERE tmdb_id=?',
(tmdb_id,),
)
except Exception:
# 兼容旧版本数据库(可能没有 air_date_offset 或 air_date_offset_manually_set 字段)
try:
# 先尝试查询包含 air_date_offset 但不包含 air_date_offset_manually_set 的情况
try:
cursor.execute(
'SELECT local_air_time, air_time_source, air_timezone, air_date_offset FROM shows WHERE tmdb_id=?',
(tmdb_id,),
)
row = cursor.fetchone()
if not row:
return None
air_time_source = (row[1] or "").strip() if row[1] is not None else ""
air_timezone = (row[2] or "").strip() if row[2] is not None else ""
air_date_offset = int(row[3]) if row[3] is not None else 0
air_date_offset_manually_set = 0 # 旧数据默认为未手动设置
return {
"local_air_time": (row[0] or "").strip() if row[0] is not None else "",
"air_time_source": air_time_source,
"air_timezone": air_timezone,
"air_date_offset": air_date_offset,
"air_date_offset_manually_set": bool(air_date_offset_manually_set),
}
except Exception:
# 如果连 air_date_offset 字段都没有,使用最旧的查询方式
cursor.execute(
'SELECT local_air_time, air_time_source, air_timezone FROM shows WHERE tmdb_id=?',
(tmdb_id,),
)
row = cursor.fetchone()
if not row:
return None
air_time_source = (row[1] or "").strip() if row[1] is not None else ""
air_timezone = (row[2] or "").strip() if row[2] is not None else ""
air_date_offset = 0
air_date_offset_manually_set = 0
return {
"local_air_time": (row[0] or "").strip() if row[0] is not None else "",
"air_time_source": air_time_source,
"air_timezone": air_timezone,
"air_date_offset": air_date_offset,
"air_date_offset_manually_set": bool(air_date_offset_manually_set),
}
except Exception:
return None
row = cursor.fetchone()
if not row:
return None
air_time_source = (row[1] or "").strip() if row[1] is not None else ""
air_timezone = (row[2] or "").strip() if row[2] is not None else ""
air_date_offset = int(row[3]) if row[3] is not None else 0
air_date_offset_manually_set = int(row[4]) if row[4] is not None else 0
# 重要:不要自动计算并覆盖偏移值!
# 如果用户手动设置了偏移值即使为0应该保持原值
# 只有在确实没有偏移值NULL且有时区信息时才尝试自动计算
# 但这里我们不再自动计算,因为可能会覆盖用户手动设置的值
# 如果需要自动计算,应该在初始同步时进行,而不是在读取时
result = {
"local_air_time": (row[0] or "").strip() if row[0] is not None else "",
"air_time_source": air_time_source,
"air_timezone": air_timezone,
"air_date_offset": air_date_offset,
"air_date_offset_manually_set": bool(air_date_offset_manually_set),
}
return result
def _calculate_date_offset_from_existing_episodes(self, tmdb_id: int) -> int:
"""
从已有集数据计算日期偏移用于自动补上旧数据的偏移值
"""
try:
cursor = self.conn.cursor()
# 获取最新季的前几集,比较 air_date 和 air_date_local
cursor.execute(
"""
SELECT e.air_date, e.air_date_local
FROM episodes e
INNER JOIN shows s ON e.tmdb_id = s.tmdb_id
WHERE e.tmdb_id=? AND e.season_number=?
AND e.air_date IS NOT NULL AND e.air_date != ''
AND e.air_date_local IS NOT NULL AND e.air_date_local != ''
ORDER BY e.episode_number ASC
LIMIT 5
""",
(int(tmdb_id), self.get_show(int(tmdb_id)).get('latest_season_number') or 1)
)
rows = cursor.fetchall()
if not rows:
return 0
# 计算每集的日期差异,取最常见的偏移值
offsets = []
from datetime import datetime as _dt
for air_date, air_date_local in rows:
try:
date_orig = _dt.strptime(str(air_date), "%Y-%m-%d").date()
date_local = _dt.strptime(str(air_date_local), "%Y-%m-%d").date()
offset = (date_local - date_orig).days
offsets.append(offset)
except Exception:
continue
if not offsets:
return 0
# 返回最常见的偏移值(如果所有集的偏移都相同,则使用该值)
if len(set(offsets)) == 1:
return offsets[0]
# 如果偏移不一致,返回最常见的偏移值
from collections import Counter
most_common = Counter(offsets).most_common(1)
if most_common:
return most_common[0][0]
return 0
except Exception:
return 0
@retry_on_locked(max_retries=3, base_delay=0.1) @retry_on_locked(max_retries=3, base_delay=0.1)
def get_shows_by_content_type(self, content_type:str): def get_shows_by_content_type(self, content_type:str):
"""根据内容类型获取节目列表""" """根据内容类型获取节目列表"""

View File

@ -128,6 +128,20 @@ class TMDBService:
return result['results'][0] return result['results'][0]
return None return None
def search_tv_show_all(self, query: str, year: str = None) -> List[Dict]:
"""搜索电视剧,返回所有搜索结果"""
params = {
'query': query,
}
# 如果提供了年份,添加到参数中
if year:
params['first_air_date_year'] = year
result = self._make_request('/search/tv', params)
if result and result.get('results'):
return result['results']
return []
def get_tv_show_details(self, tv_id: int) -> Optional[Dict]: def get_tv_show_details(self, tv_id: int) -> Optional[Dict]:
"""获取电视剧详细信息""" """获取电视剧详细信息"""
return self._make_request(f'/tv/{tv_id}') return self._make_request(f'/tv/{tv_id}')

189
app/sdk/trakt_service.py Normal file
View File

@ -0,0 +1,189 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Trakt 服务模块
基于 TMDB ID 映射到 Trakt 节目并使用节目级 aired_time + timezone 获取精确播出时间
再转换为本地时区的统一播出时间格式HH:MM
"""
import logging
from datetime import datetime, date, time as dtime
from typing import Optional, Dict, Any
import requests
try:
# Python 3.9+ 标准库时区支持
from zoneinfo import ZoneInfo
except Exception: # pragma: no cover - 兼容极老环境
ZoneInfo = None # type: ignore
logger = logging.getLogger(__name__)
class TraktService:
"""
Trakt API 轻量封装
- 通过 TMDB ID 查找 Trakt 节目
- 获取节目级播出时间 aired_time + timezone
- 将播出地时区的 daily airtime 转换为本地时区 HH:MM
"""
def __init__(self, client_id: Optional[str] = None, base_url: str = "https://api.trakt.tv"):
self.client_id = (client_id or "").strip()
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
# 统一请求头
self.session.headers.update(
{
"Content-Type": "application/json",
"trakt-api-version": "2",
"trakt-api-key": self.client_id or "",
}
)
def is_configured(self) -> bool:
"""检查 Trakt 是否已配置有效的 Client ID。"""
return bool(self.client_id)
# ----------------- HTTP 基础封装 -----------------
def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Any]:
"""发起 GET 请求,失败时记录日志并返回 None不抛出到上层。"""
if not self.is_configured():
return None
url = f"{self.base_url}{path}"
try:
resp = self.session.get(url, params=params or {}, timeout=10)
resp.raise_for_status()
return resp.json()
except Exception as e:
logger.warning(f"Trakt GET 请求失败: {url}, err={e}")
return None
# ----------------- 节目级信息 -----------------
def get_show_by_tmdb_id(self, tmdb_id: int) -> Optional[Dict[str, Any]]:
"""
通过 TMDB ID 查找 Trakt 节目
返回结构示例
{
"trakt_id": 195268,
"slug": "it-welcome-to-derry",
"title": "IT: Welcome to Derry",
"year": 2025
}
"""
try:
if not self.is_configured():
return None
if not tmdb_id:
return None
path = f"/search/tmdb/{int(tmdb_id)}"
data = self._get(path, params={"type": "show"})
if not data:
return None
# Trakt 搜索返回列表,取第一个 show 结果
item = None
for entry in data:
if entry.get("type") == "show" and entry.get("show"):
item = entry.get("show") or {}
break
if not item:
return None
return {
"trakt_id": item.get("ids", {}).get("trakt"),
"slug": item.get("ids", {}).get("slug"),
"title": item.get("title") or "",
"year": item.get("year"),
}
except Exception as e:
logger.warning(f"通过 TMDB ID 获取 Trakt 节目失败: tmdb_id={tmdb_id}, err={e}")
return None
def get_show_airtime(self, trakt_show_id: Any) -> Optional[Dict[str, str]]:
"""
获取节目级播出时间信息aired_time + timezone
Trakt 节目详情通常包含:
{
"airs": {
"day": "sunday",
"time": "21:00",
"timezone": "America/New_York"
},
...
}
"""
try:
if not self.is_configured():
return None
if not trakt_show_id:
return None
path = f"/shows/{trakt_show_id}"
data = self._get(path, params={"extended": "full"})
if not data:
return None
airs = data.get("airs") or {}
aired_time = (airs.get("time") or "").strip()
timezone = (airs.get("timezone") or "").strip()
if not aired_time or not timezone:
return None
return {"aired_time": aired_time, "timezone": timezone}
except Exception as e:
logger.warning(f"获取 Trakt 节目播出时间失败: trakt_show_id={trakt_show_id}, err={e}")
return None
# ----------------- 时区转换 -----------------
def convert_show_airtime_to_local(
self, aired_time: str, source_tz: str, local_tz: str
) -> Optional[str]:
"""
将播出地时区的 daily airtime 转换为本地时区的 HH:MM
参数:
aired_time: 播出地时间字符串形式 '21:00'
source_tz: 播出地时区 'America/New_York'
local_tz: 本地时区 'Asia/Shanghai'
返回:
本地时区 HH:MM 字符串失败时返回 None
"""
try:
aired_time = (aired_time or "").strip()
source_tz = (source_tz or "").strip()
local_tz = (local_tz or "").strip()
if not aired_time or not source_tz or not local_tz:
return None
if ZoneInfo is None:
# 环境不支持 zoneinfo 时,退化为直接返回原始播出时间
return aired_time
# 解析 HH:MM
try:
hh, mm = [int(x) for x in aired_time.split(":")]
except Exception:
return None
# 使用任意日期承载“每日播出时间”含义,这里选择今天
today = date.today()
naive_dt = datetime.combine(today, dtime(hour=hh, minute=mm))
try:
src_zone = ZoneInfo(source_tz)
dst_zone = ZoneInfo(local_tz)
except Exception:
# 时区字符串非法时,保守返回原 time
return aired_time
src_dt = naive_dt.replace(tzinfo=src_zone)
local_dt = src_dt.astimezone(dst_zone)
return local_dt.strftime("%H:%M")
except Exception as e:
logger.warning(
f"转换节目播出时间到本地时区失败: aired_time={aired_time}, source_tz={source_tz}, local_tz={local_tz}, err={e}"
)
return None
# 方便其它模块直接导入一个全局实例时再按需注入 client_id
trakt_service: Optional[TraktService] = None

View File

@ -14,12 +14,13 @@
--focus-border-color: #0D53FF; /* 输入框聚焦时的边框颜色 */ --focus-border-color: #0D53FF; /* 输入框聚焦时的边框颜色 */
--shadow-spread: 0; /* 统一阴影扩散距离设为0 */ --shadow-spread: 0; /* 统一阴影扩散距离设为0 */
--button-gray-background-color: #ededf0; /* 按钮灰色背景颜色 */ --button-gray-background-color: #ededf0; /* 按钮灰色背景颜色 */
--modal-border-radius: 12px; /* 模态框弹窗和登录模块的统一圆角 */
} }
/* --------------- 基础样式 --------------- */ /* --------------- 基础样式 --------------- */
body { body {
font-size: 1rem; font-size: 1rem;
padding-bottom: 110px; padding-bottom: 15px;
color: var(--dark-text-color); color: var(--dark-text-color);
} }
@ -188,8 +189,13 @@ main .row.title {
margin-left: -15px; /* 添加负边距向左移动2px */ margin-left: -15px; /* 添加负边距向左移动2px */
} }
/* 精准调整QASX API 模块与上方模块的垂直间距减小 8px */ /* 精准调整:性能设置模块与上方模块的垂直间距减小 8px */
main .row.title[title^="QASX API"] { main .row.title[title*="配置文件加载、数据缓存和自动刷新的关键参数"] {
margin-top: 12px;
}
/* 精准调整API 模块与上方模块的垂直间距减小 8px */
main .row.title[title*="配置与任务处理、节目元数据获取及外部服务交互相关的API访问凭证"] {
margin-top: 12px; margin-top: 12px;
} }
@ -202,6 +208,7 @@ main .row.title h2 {
margin-top: 0; margin-top: 0;
line-height: 1.5; line-height: 1.5;
padding-left: 0px; /* 标题文字左内边距 */ padding-left: 0px; /* 标题文字左内边距 */
cursor: default; /* 不可操作的模块大标题使用普通指针 */
} }
/* 标题旁边的问号图标容器样式 */ /* 标题旁边的问号图标容器样式 */
@ -1104,6 +1111,7 @@ select.form-control {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; /* 可点击的图标按钮使用指针 */
} }
.input-group-text:has(.bi-google):hover, .input-group-text:has(.bi-google):hover,
@ -1166,6 +1174,7 @@ textarea.form-control {
border-color: var(--border-color) !important; /* 使用变量替代硬编码颜色 */ border-color: var(--border-color) !important; /* 使用变量替代硬编码颜色 */
background-color: #ededf0!important; /* 修改背景色为更浅的灰色 */ background-color: #ededf0!important; /* 修改背景色为更浅的灰色 */
border-width: 1px !important; /* 确保边框宽度为1px */ border-width: 1px !important; /* 确保边框宽度为1px */
cursor: default; /* 不可操作的配置选项标题使用普通指针 */
} }
.input-group-prepend .input-group-text { .input-group-prepend .input-group-text {
@ -1253,6 +1262,7 @@ table.table thead th {
height: 40px !important; /* 确保表头高度为40px */ height: 40px !important; /* 确保表头高度为40px */
line-height: 24px !important; /* 设置行高以确保文字垂直居中 */ line-height: 24px !important; /* 设置行高以确保文字垂直居中 */
box-sizing: border-box !important; /* 确保边框包含在总高度内 */ box-sizing: border-box !important; /* 确保边框包含在总高度内 */
cursor: default; /* 不可操作的表头使用普通指针 */
} }
/* 表头悬停样式 */ /* 表头悬停样式 */
@ -1457,7 +1467,7 @@ button.close:focus,
} }
#logModal .modal-content { #logModal .modal-content {
border-radius: 6px; border-radius: var(--modal-border-radius);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1);
} }
@ -1467,8 +1477,8 @@ button.close:focus,
background-color: #fff; background-color: #fff;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: 11px 16px; padding: 11px 16px;
border-top-left-radius: 6px; border-top-left-radius: var(--modal-border-radius);
border-top-right-radius: 6px; border-top-right-radius: var(--modal-border-radius);
} }
#logModal .modal-title { #logModal .modal-title {
@ -1477,6 +1487,7 @@ button.close:focus,
color: var(--dark-text-color); color: var(--dark-text-color);
display: flex; display: flex;
align-items: center; align-items: center;
cursor: default; /* 不可操作的模态框标题使用普通指针 */
} }
#logModal .modal-title b { #logModal .modal-title b {
@ -1546,7 +1557,7 @@ button.close:focus,
#fileSelectModal .modal-content, #fileSelectModal .modal-content,
#createTaskModal .modal-content, #createTaskModal .modal-content,
#editMetadataModal .modal-content { #editMetadataModal .modal-content {
border-radius: 6px; border-radius: var(--modal-border-radius);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1);
} }
@ -1558,8 +1569,8 @@ button.close:focus,
background-color: #fff; background-color: #fff;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: 11px 16px; padding: 11px 16px;
border-top-left-radius: 6px; border-top-left-radius: var(--modal-border-radius);
border-top-right-radius: 6px; border-top-right-radius: var(--modal-border-radius);
} }
#fileSelectModal .modal-title, #fileSelectModal .modal-title,
@ -1570,6 +1581,7 @@ button.close:focus,
color: var(--dark-text-color); color: var(--dark-text-color);
display: flex; display: flex;
align-items: center; align-items: center;
cursor: default; /* 不可操作的模态框标题使用普通指针 */
} }
#fileSelectModal .modal-title b, #fileSelectModal .modal-title b,
@ -1663,6 +1675,7 @@ button.close:focus,
/* 编辑元数据模态框:输入前标题灰底,统一输入高度与字体,与创建任务保持一致 */ /* 编辑元数据模态框:输入前标题灰底,统一输入高度与字体,与创建任务保持一致 */
#editMetadataModal .input-group-prepend .input-group-text { #editMetadataModal .input-group-prepend .input-group-text {
background-color: var(--button-gray-background-color) !important; background-color: var(--button-gray-background-color) !important;
cursor: default; /* 不可操作的前缀文本使用普通指针 */
} }
#editMetadataModal .form-control, #editMetadataModal .form-control,
#editMetadataModal .input-group-text, #editMetadataModal .input-group-text,
@ -1839,6 +1852,7 @@ button.close:focus,
z-index: 5; z-index: 5;
vertical-align: middle; /* 添加:垂直居中对齐 */ vertical-align: middle; /* 添加:垂直居中对齐 */
height: 40px !important; /* 添加:自动高度,确保与内容一致 */ height: 40px !important; /* 添加:自动高度,确保与内容一致 */
cursor: default; /* 不可操作的表头使用普通指针 */
} }
/* 模态框表格列宽设置 - 基于内容类型 */ /* 模态框表格列宽设置 - 基于内容类型 */
@ -2253,7 +2267,7 @@ div.jsoneditor-tree button.jsoneditor-button:focus {
.login-card { .login-card {
width: 340px; width: 340px;
background-color: white; background-color: white;
border-radius: 10px; border-radius: var(--modal-border-radius);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
padding: 0; padding: 0;
text-align: center; text-align: center;
@ -2394,7 +2408,7 @@ div.jsoneditor-tree button.jsoneditor-button:focus {
transition: color 0.2s; /* 添加颜色过渡效果 */ transition: color 0.2s; /* 添加颜色过渡效果 */
} }
/* 侧边栏菜单项图标样式 */ /* 侧边栏菜导航图标样式 */
.sidebar .nav-link .bi-list-ul { .sidebar .nav-link .bi-list-ul {
font-size: 1.1rem; font-size: 1.1rem;
position: relative; position: relative;
@ -2423,8 +2437,12 @@ div.jsoneditor-tree button.jsoneditor-button:focus {
font-size: 0.94rem; font-size: 0.94rem;
} }
.sidebar .nav-link .bi-terminal {
font-size: 0.95rem;
}
.sidebar .nav-link .bi-power { .sidebar .nav-link .bi-power {
font-size: 1.27rem; font-size: 1.26rem;
} }
.bottom-links .nav-link .bi-book { .bottom-links .nav-link .bi-book {
@ -2724,6 +2742,11 @@ body {
color: var(--dark-text-color); color: var(--dark-text-color);
} }
/* 所有模态框标题使用普通指针 */
.modal-title {
cursor: default; /* 不可操作的模态框标题使用普通指针 */
}
/* 设置输入框占位符的颜色 */ /* 设置输入框占位符的颜色 */
.form-control::placeholder { .form-control::placeholder {
color: var(--light-text-color); /* 修改占位符颜色 */ color: var(--light-text-color); /* 修改占位符颜色 */
@ -4913,6 +4936,17 @@ select.task-filter-select,
max-width: 80%; max-width: 80%;
} }
#batchRenameModal .modal-content {
border-radius: var(--modal-border-radius);
border: 1px solid var(--border-color);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1);
}
#batchRenameModal .modal-header {
border-top-left-radius: var(--modal-border-radius);
border-top-right-radius: var(--modal-border-radius);
}
#batchRenameModal .table { #batchRenameModal .table {
margin-bottom: 0; margin-bottom: 0;
} }
@ -5227,6 +5261,7 @@ table.selectable-files th {
height: 40px !important; /* 确保表头高度为40px */ height: 40px !important; /* 确保表头高度为40px */
line-height: 24px !important; /* 设置行高以确保文字垂直居中 */ line-height: 24px !important; /* 设置行高以确保文字垂直居中 */
box-sizing: border-box !important; /* 确保边框包含在总高度内 */ box-sizing: border-box !important; /* 确保边框包含在总高度内 */
cursor: default; /* 不可操作的表头使用普通指针 */
} }
/* 文件整理页面表格单元格样式,与转存记录页面保持一致 */ /* 文件整理页面表格单元格样式,与转存记录页面保持一致 */
@ -5911,6 +5946,19 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
color: var(--dark-text-color) !important; color: var(--dark-text-color) !important;
} }
/* 文件选择模态框的取消按钮样式(通用样式,覆盖所有情况) */
#fileSelectModal .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;
}
#fileSelectModal .modal-footer .btn-cancel:hover {
background-color: #e0e2e6 !important;
border-color: #e0e2e6 !important;
color: var(--dark-text-color) !important;
}
/* --------------- 模态框层级管理 --------------- */ /* --------------- 模态框层级管理 --------------- */
/* 当从创建任务模态框中打开文件选择模态框时,确保文件选择模态框显示在上层 */ /* 当从创建任务模态框中打开文件选择模态框时,确保文件选择模态框显示在上层 */
#createTaskModal.show ~ #fileSelectModal { #createTaskModal.show ~ #fileSelectModal {
@ -6007,6 +6055,7 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
justify-content: center; justify-content: center;
padding: 0 8px; padding: 0 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
cursor: pointer; /* 可点击的按钮使用指针 */
} }
#createTaskModal .input-group-text:hover { #createTaskModal .input-group-text:hover {
@ -6020,12 +6069,14 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
background-color: var(--button-gray-background-color) !important; background-color: var(--button-gray-background-color) !important;
border-color: var(--border-color) !important; border-color: var(--border-color) !important;
color: var(--dark-text-color) !important; color: var(--dark-text-color) !important;
cursor: default; /* 不可操作的后缀文本使用普通指针 */
} }
#createTaskModal .input-group-text:has(input[type="checkbox"]):hover { #createTaskModal .input-group-text:has(input[type="checkbox"]):hover {
background-color: var(--button-gray-background-color) !important; background-color: var(--button-gray-background-color) !important;
border-color: var(--border-color) !important; border-color: var(--border-color) !important;
color: var(--dark-text-color) !important; color: var(--dark-text-color) !important;
cursor: default; /* 悬停时也保持普通指针 */
} }
/* 创建任务模态框中的按钮样式 - 完全复制任务列表样式 */ /* 创建任务模态框中的按钮样式 - 完全复制任务列表样式 */
@ -6521,8 +6572,24 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
.discovery-rating.status-ended { color: #ff4d4f; } /* 红色 */ .discovery-rating.status-ended { color: #ff4d4f; } /* 红色 */
.discovery-rating.status-other { color: #00C853; } /* 绿色 */ .discovery-rating.status-other { color: #00C853; } /* 绿色 */
/* 追剧日历 - 已转存标识 */ /* 追剧日历 - 已转存/已播出标识 */
.calendar-transferred-badge { color: #00C853; padding: 2px 8px 2px 7.5px; } .calendar-transferred-badge {
color: #00C853;
padding: 2px 0; /* 基础上下 padding左右 padding 由子类设置 */
}
/* 已转存集 */
.calendar-transferred-badge-transferred {
padding-left: 9px;
padding-right: 10px;
}
/* 已播出未转存集 */
.calendar-transferred-badge-aired {
padding-left: 7.5px;
padding-right: 8px;
}
.calendar-transferred-badge i { .calendar-transferred-badge i {
line-height: 1; line-height: 1;
display: inline-block; display: inline-block;
@ -6530,10 +6597,10 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
/* 使用多向阴影模拟加粗图标为字体font-weight不生效 */ /* 使用多向阴影模拟加粗图标为字体font-weight不生效 */
text-shadow: text-shadow:
0 0 0 currentColor, 0 0 0 currentColor,
0 0.45px 0 currentColor, 0 0.3px 0 currentColor,
0 -0.45px 0 currentColor, 0 -0.3px 0 currentColor,
0.45px 0 0 currentColor, 0.3px 0 0 currentColor,
-0.45px 0 0 currentColor; -0.3px 0 0 currentColor;
} }
.discovery-create-task { .discovery-create-task {
@ -6744,6 +6811,7 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
cursor: default; /* 不可操作的类型/集数信息使用普通指针 */
} }
.genre-slash { .genre-slash {
@ -6791,6 +6859,12 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
} }
} }
/* 影视发现页面与任务列表海报视图统一底部边距 */
/* 使用 .discovery-controls 作为标识因为只有影视发现页面有这个class */
.discovery-controls ~ * .discovery-grid {
margin-bottom: 0.5px;
}
/* 文件整理页面命名预览模式下的展开状态文本位置调整 - 最高优先级 */ /* 文件整理页面命名预览模式下的展开状态文本位置调整 - 最高优先级 */
#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename > div[style*="white-space: normal"][style*="word-break: break-word"] { #fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename > div[style*="white-space: normal"][style*="word-break: break-word"] {
position: relative !important; position: relative !important;
@ -7025,6 +7099,11 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
.calendar-controls .btn-group { .calendar-controls .btn-group {
margin: 0; /* 由 gap 控制间距 */ margin: 0; /* 由 gap 控制间距 */
} }
/* 追剧日历页面的排序组件:覆盖任务列表的 -4px 偏移,保持与其他按钮对齐 */
.calendar-controls .tasklist-sort-controls {
margin-top: 0 !important;
}
} }
/* 统一日历控制按钮的样式 */ /* 统一日历控制按钮的样式 */
@ -7117,10 +7196,17 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
height: 32px; height: 32px;
font-size: 0.95rem; font-size: 0.95rem;
color: var(--dark-text-color); color: var(--dark-text-color);
cursor: default; cursor: pointer;
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
} }
.calendar-date-item.today { .calendar-date-item:hover:not(.selected) {
background-color: var(--dark-text-color);
border-color: var(--dark-text-color);
color: white;
}
.calendar-date-item.selected {
background-color: var(--focus-border-color); background-color: var(--focus-border-color);
border-color: var(--focus-border-color); border-color: var(--focus-border-color);
color: white; color: white;
@ -7302,12 +7388,13 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
font-size: 0.95rem; font-size: 0.95rem;
color: var(--dark-text-color); color: var(--dark-text-color);
border-right: 1px solid var(--border-color); /* 显示内部分割线 */ border-right: 1px solid var(--border-color); /* 显示内部分割线 */
background-color: #f7f7f9; background-color: var(--button-gray-background-color);
/* 防止文本被挤压成竖排 */ /* 防止文本被挤压成竖排 */
min-width: 100px; min-width: 100px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
cursor: default; /* 不可操作的星期表头使用普通指针 */
} }
/* 移除星期导航最后一列的右侧分割线,避免溢出 */ /* 移除星期导航最后一列的右侧分割线,避免溢出 */
@ -7386,6 +7473,13 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
display: none; /* 隐藏蓝色圆形背景 */ display: none; /* 隐藏蓝色圆形背景 */
} }
/* 桌面端:选中日期的号数高亮显示(使用 focus-border-color */
@media (min-width: 577px) {
.calendar-month-cell.selected .calendar-month-date {
color: var(--focus-border-color);
}
}
.calendar-month-cell.has-episodes { .calendar-month-cell.has-episodes {
background-color: transparent !important; /* 去除有播出集背景色 */ background-color: transparent !important; /* 去除有播出集背景色 */
} }
@ -7402,7 +7496,7 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
font-size: 0.85rem; font-size: 0.85rem;
padding: 6px; /* 等距6px */ padding: 6px; /* 等距6px */
line-height: 1; /* 统一行高,消除上下视觉不等距 */ line-height: 1; /* 统一行高,消除上下视觉不等距 */
/* 日历视图:默认保持原按钮灰背景 */ /* 日历视图:默认保持原按钮灰背景(未转存且未播出) */
background-color: #f7f7f9; background-color: #f7f7f9;
border-radius: 6px; /* 圆角6px */ border-radius: 6px; /* 圆角6px */
display: flex; display: flex;
@ -7410,11 +7504,16 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
gap: 4px; gap: 4px;
} }
/* 日历视图:当该集已达转存进度时,卡片背景改为导航悬停浅蓝色 */ /* 日历视图:当该集已达转存进度时,卡片背景改为导航悬停浅蓝色 */
.calendar-month-episode:has(.episode-number.episode-number-reached) { .calendar-month-episode:has(.episode-number.episode-number-reached) {
background-color: #e6f1ff; background-color: #e6f1ff;
} }
/* 日历视图:未转存但已播出的集,卡片背景为浅绿色 */
.calendar-month-episode:has(.episode-number.episode-number-aired):not(:has(.episode-number.episode-number-reached)) {
background-color: #E6F7EE;
}
.episode-title { .episode-title {
font-weight: 400; /* 常规体 */ font-weight: 400; /* 常规体 */
flex: 1; flex: 1;
@ -7584,11 +7683,14 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
} }
/* 超小模式:表格内部的卡片背景色覆盖(仅表格体内紧凑卡片) */ /* 超小模式:表格内部的卡片背景色覆盖(仅表格体内紧凑卡片) */
.calendar-month-body .calendar-month-episode { .calendar-month-body .calendar-month-episode {
background-color: var(--border-color) !important; /* 普通状态背景色 */ background-color: var(--border-color) !important; /* 普通状态背景色(未转存且未播出) */
} }
.calendar-month-body .calendar-month-episode:has(.episode-number.episode-number-reached) { .calendar-month-body .calendar-month-episode:has(.episode-number.episode-number-reached) {
background-color: var(--focus-border-color) !important; /* 已转存背景色 */ background-color: var(--focus-border-color) !important; /* 已转存背景色 */
} }
.calendar-month-body .calendar-month-episode:has(.episode-number.episode-number-aired):not(:has(.episode-number.episode-number-reached)) {
background-color: #28a745 !important; /* 已播出但未转存背景色 */
}
} }
@ -7603,12 +7705,15 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
padding: 6px; padding: 6px;
line-height: 1; line-height: 1;
transform: none; /* 还原不上移 */ transform: none; /* 还原不上移 */
background-color: #f7f7f9; /* 桌面端默认背景 */ background-color: #f7f7f9; /* 桌面端默认背景:未转存且未播出 */
border-radius: 6px; border-radius: 6px;
} }
.calendar-selected-episodes .calendar-month-episode:has(.episode-number.episode-number-reached) { .calendar-selected-episodes .calendar-month-episode:has(.episode-number.episode-number-reached) {
background-color: #e6f1ff; /* 桌面端已转存背景 */ background-color: #e6f1ff; /* 桌面端已转存背景 */
} }
.calendar-selected-episodes .calendar-month-episode:has(.episode-number.episode-number-aired):not(:has(.episode-number.episode-number-reached)) {
background-color: #E6F7EE; /* 桌面端已播出但未转存背景 */
}
/* 移动端:下方列表的集号样式与桌面端一致(显示完整 SxxExx */ /* 移动端:下方列表的集号样式与桌面端一致(显示完整 SxxExx */
.calendar-selected-episodes .calendar-month-episode .episode-number { .calendar-selected-episodes .calendar-month-episode .episode-number {
@ -7875,11 +7980,16 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
border-color: #0A42CC !important; border-color: #0A42CC !important;
} }
/* 显示设置:拖拽时显示“移动”而非“添加”视觉提示 */ /* 显示设置:拖拽时显示"移动"而非"添加"视觉提示 */
.draggable-item { .draggable-item {
cursor: move; /* 显示移动光标 */ cursor: move; /* 显示移动光标 */
} }
/* 显示设置可拖动配置项的标题也使用拖拽指针 */
.draggable-item .input-group-text {
cursor: move !important; /* 可拖动配置项的标题使用拖拽指针 */
}
.draggable-item:active { .draggable-item:active {
opacity: 1; opacity: 1;
} }
@ -7894,10 +8004,16 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
cursor: text; cursor: text;
} }
/* QASX API Token 显示框背景色:与普通输入框保持一致,覆盖 disabled 状态的灰色背景 */
.form-control.token-display:disabled,
.form-control.token-display[readonly] {
background-color: #fff !important; /* 与普通输入框背景色保持一致 */
}
/* TMDB 说明文本样式与链接样式(继承颜色、无下划线、悬停不变) */ /* TMDB 说明文本样式与链接样式(继承颜色、无下划线、悬停不变) */
.tmdb-attribution { .tmdb-attribution {
margin-top: 4px; margin-top: 4px;
margin-bottom: 4px; margin-bottom: 2px;
color: var(--light-text-color); color: var(--light-text-color);
} }
.tmdb-attribution a { .tmdb-attribution a {
@ -7907,7 +8023,7 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
.tmdb-attribution a:hover, .tmdb-attribution a:hover,
.tmdb-attribution a:focus { .tmdb-attribution a:focus {
text-decoration: none; text-decoration: none;
color: inherit; color: var(--focus-border-color);
} }
/* 任务列表:类型筛选按钮与上方名称筛选区域的间距 */ /* 任务列表:类型筛选按钮与上方名称筛选区域的间距 */
@ -7941,7 +8057,7 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
font-size: 0.95rem; /* 与左侧类型按钮一致 */ font-size: 0.95rem; /* 与左侧类型按钮一致 */
} }
/* 左侧“按”按钮:保留边框,去右边框,与中间边框重叠 */ /* 左侧"按"按钮:保留边框,去右边框,与中间边框重叠 */
.tasklist-sort-pill-icon { .tasklist-sort-pill-icon {
width: 31px; width: 31px;
min-width: 31px; min-width: 31px;
@ -7950,6 +8066,7 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
border-right: none !important; border-right: none !important;
border-radius: 6px 0 0 6px; border-radius: 6px 0 0 6px;
background-color: var(--button-gray-background-color); background-color: var(--button-gray-background-color);
cursor: default; /* 不可操作的"按"按钮使用普通指针 */
} }
/* 中间下拉:白底,负责显示唯一可见边框(上下左右均有) */ /* 中间下拉:白底,负责显示唯一可见边框(上下左右均有) */
@ -7961,7 +8078,7 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
background: #fff !important; background: #fff !important;
cursor: pointer; cursor: default;
background-image: none; background-image: none;
height: 32px; height: 32px;
line-height: 30px; line-height: 30px;
@ -8409,10 +8526,131 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
} }
.tasklist-count-number { .tasklist-count-number {
cursor: text; cursor: default;
user-select: text; user-select: text;
} }
/* 追剧日历计数模块减少左侧与排序组件的间距8pxml-2是8px减少8px后为0 */
.calendar-count-indicator {
margin-left: 0 !important;
}
/* 运行日志页面日志行样式:与运行日志弹窗保持一致 */
.runlog-content .runlog-line {
margin: 0;
padding: 0 0 0 1.5px; /* 桌面端日志行左边距 */
font-family: monospace;
font-size: 0.85rem; /* 与 #logModal pre 字号一致 */
line-height: 1.5; /* 行高与弹窗一致 */
color: var(--dark-text-color);
/* 桌面端:超长内容不换行,直接截断显示 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 运行日志页面:级别可点击筛选样式 */
.runlog-content .log-level-clickable {
cursor: pointer;
transition: color 0.2s ease-in-out;
user-select: text; /* 允许选中文本,确保复制时包含级别信息 */
}
.runlog-content .log-level-clickable:hover {
color: var(--focus-border-color) !important;
}
/* 运行日志页面:内容中可点击的快速筛选元素样式(>>> 和任务名称) */
.runlog-content .runtime-log-arrow-clickable,
.runlog-content .runtime-log-taskname-clickable {
cursor: pointer;
user-select: text; /* 允许正常选中复制文本 */
}
.runlog-content .runtime-log-arrow-clickable:hover,
.runlog-content .runtime-log-taskname-clickable:hover {
color: var(--focus-border-color);
}
/* --------------- 页面底部元素统一间距 --------------- */
/* 转存记录和文件整理页面:分页控制区域距离页面底部统一为 20px */
.pagination-container {
margin-bottom: -90px !important;
}
/* 日历表格距离页面底部为 20px */
/* 同时覆盖 padding-bottom确保样式生效 */
.calendar-month-mode {
margin-bottom: -86px !important;
padding-bottom: 20px !important;
}
.calendar-filter-row { .calendar-filter-row {
margin-bottom: 20px; /* 桌面端保持与下方组件净间距 8px抵消分类与控制按钮的 -12px 上移) */ margin-bottom: 20px;
}
/* --------------- 运行日志页面 --------------- */
.runlog-content {
min-height: 360px;
max-height: calc(100vh - 146px);
overflow-y: auto;
/* 通过负margin抵消body的padding-bottom(15px)确保日志显示区域底部距离页面底部为20px */
margin-bottom: -15px !important;
padding: 0 0 20px 0;
}
@media (max-width: 768px) {
.runlog-content {
max-height: none;
min-height: 240px;
/* 窄屏设备:支持横向滚动,完整显示超长内容 */
overflow-x: auto;
}
/* 窄屏设备:日志行允许横向滚动,完整显示内容 */
.runlog-content .runlog-line {
white-space: nowrap; /* 不换行,保持单行 */
overflow: unset; /* 移除overflow限制允许内容溢出到父容器 */
text-overflow: unset; /* 移除省略号,完整显示 */
padding-left: 5.5px; /* 窄屏设备日志行左边距 */
}
}
/* API 配置模块样式 */
.api-config-group + .api-config-group {
margin-top: 8px;
}
.api-label-link {
text-decoration: none;
color: inherit;
cursor: pointer;
}
.api-label-link:hover,
.api-label-link:focus {
color: var(--focus-border-color);
text-decoration: none;
}
/* 海报悬停信息行数限制样式 */
/* 单行限制:最多显示一行,超长部分截断显示省略号 */
.discovery-poster-overlay .info-line-single,
.calendar-poster-overlay .info-line-single {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
/* 双行限制:最多显示两行,如果两行还不能显示完整,超长部分截断显示省略号 */
.discovery-poster-overlay .info-line-double,
.calendar-poster-overlay .info-line-double {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: normal;
word-wrap: break-word;
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,6 @@ import re
import os import os
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from datetime import datetime from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class TaskExtractor: class TaskExtractor:
def __init__(self): def __init__(self):
# 剧集编号提取模式 # 剧集编号提取模式
@ -233,31 +229,19 @@ class TaskExtractor:
Returns: Returns:
包含所有任务信息的列表 包含所有任务信息的列表
""" """
logging.debug("TaskExtractor.extract_all_tasks_info 开始")
logging.debug(f"tasks数量: {len(tasks)}")
logging.debug(f"task_latest_files数量: {len(task_latest_files)}")
tasks_info = [] tasks_info = []
for i, task in enumerate(tasks): for i, task in enumerate(tasks):
try: try:
logging.debug(f"处理第{i+1}个任务: {task.get('taskname', '')}")
task_name = task.get('taskname', '') task_name = task.get('taskname', '')
save_path = task.get('savepath', '') save_path = task.get('savepath', '')
latest_file = task_latest_files.get(task_name, '') latest_file = task_latest_files.get(task_name, '')
logging.debug(f"task_name: {task_name}")
logging.debug(f"save_path: {save_path}")
logging.debug(f"latest_file: {latest_file}")
# 提取基本信息 # 提取基本信息
show_info = self.extract_show_info_from_path(save_path) show_info = self.extract_show_info_from_path(save_path)
logging.debug(f"show_info: {show_info}")
# 提取进度信息 # 提取进度信息
progress_info = self.extract_progress_from_latest_file(latest_file) progress_info = self.extract_progress_from_latest_file(latest_file)
logging.debug(f"progress_info: {progress_info}")
# 优先使用任务显式类型(配置或提取出的),否则回退到路径判断 # 优先使用任务显式类型(配置或提取出的),否则回退到路径判断
explicit_type = None explicit_type = None
@ -283,16 +267,11 @@ class TaskExtractor:
'progress_type': progress_info.get('progress_type') 'progress_type': progress_info.get('progress_type')
} }
logging.debug(f"task_info: {task_info}")
tasks_info.append(task_info) tasks_info.append(task_info)
except Exception as e: except Exception as e:
logging.debug(f"处理任务 {i+1} 时出错: {e}")
import traceback
traceback.print_exc()
continue continue
logging.debug(f"TaskExtractor.extract_all_tasks_info 完成,返回任务数量: {len(tasks_info)}")
return tasks_info return tasks_info
def get_content_type_display_name(self, content_type: str) -> str: def get_content_type_display_name(self, content_type: str) -> str: