mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-12 07:10:44 +08:00
Compare commits
36 Commits
664b818f03
...
ce8659bb01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce8659bb01 | ||
|
|
297282f24b | ||
|
|
91de6e2ae3 | ||
|
|
f0b5a98cf8 | ||
|
|
f7814dd0a3 | ||
|
|
4771f2545d | ||
|
|
b682017499 | ||
|
|
b3e525bb9b | ||
|
|
05002019fb | ||
|
|
3d3e5fb233 | ||
|
|
0972a60d91 | ||
|
|
9ac84e22c7 | ||
|
|
8b2a88897a | ||
|
|
62d2b6e739 | ||
|
|
c999913e9e | ||
|
|
e6634a361e | ||
|
|
3adcc2cc50 | ||
|
|
d1ae3a26dd | ||
|
|
645ef231bc | ||
|
|
12346fe5ec | ||
|
|
d19f8f3383 | ||
|
|
b56aec6505 | ||
|
|
dcc3943187 | ||
|
|
06adc79a17 | ||
|
|
03d9a0d0b7 | ||
|
|
4a2acea19e | ||
|
|
07abca763e | ||
|
|
8f0b079059 | ||
|
|
8c08b00db7 | ||
|
|
9269f66cd5 | ||
|
|
12cf1bb190 | ||
|
|
1045fa1934 | ||
|
|
b8e278fadb | ||
|
|
e6e1f95a8c | ||
|
|
9f2b2c7bfe | ||
|
|
a36c58986b |
@ -21,6 +21,9 @@
|
||||
- **追剧日历**:支持在追剧日历页面查看和浏览任务对应电视节目的信息和播出时间表,追踪电视节目的播出情况,了解任务的完成进度。追剧日历支持海报视图和日历视图两种视图,可方便快捷的了解订阅内容的实时状态。
|
||||
- **字幕命名规则**:支持在全局设置字幕文件的语言代码后缀,在重命名字幕文件时自动添加语言代码后缀,如 `.zh.srt`、`.zh.ass` 等。
|
||||
- **状态筛选**:支持在任务列表、转存记录、追剧日历页面按照任务状态(进度状态或对应节目状态)筛选任务(记录或节目),便于筛除次要信息。
|
||||
- **执行周期**:支持按自选周期执行(自选)和按任务进度执行(自动)两种执行周期判断模式,按任务进度执行将自动跳过进度达到 100% 的任务,也就是说如果已播出的最新集已完成转存,后续的定时任务将自动跳过该任务,如果已播出的集还有没转存的,那么这个任务会在每次的定时时间都被执行,直到进度到达 100%。如此可有效减少播出前和转存后的无意义执行。
|
||||
- **运行日志**:支持在 WebUI 上直接查看容器的运行日志,允许进行筛选和搜索。
|
||||
- **播出时间**:支持通过 Trakt API 自动获取匹配节目的准确播出时间,并转换为本地时间,也支持自定义播出时间和播出日期偏移,用于准确体现节目的播出进度和任务的完成进度,也可以用于设置任务的延迟运行。并支持在海报卡片和追剧日历上查看单集的准确播出状态(时间)和下一集的准确播出日期及时间。
|
||||
|
||||
本项目修改后的版本为个人需求定制版,目的是满足我自己的使用需求,某些(我不用的)功能可能会因为修改而出现 BUG,不一定会被修复。若你要使用本项目,请知晓本人不是程序员,我无法保证本项目的稳定性,如果你在使用过程中发现了 BUG,可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。
|
||||
|
||||
@ -54,7 +57,7 @@
|
||||
- 任务管理
|
||||
- [x] 支持多组任务
|
||||
- [x] 任务结束期限,期限后不执行此任务
|
||||
- [x] 可单独指定子任务星期几执行
|
||||
- [x] **支持按自选周期执行(自选)和按任务进度执行(自动)两种执行周期判断模式(支持混用)**
|
||||
- [x] **支持通过任务名称跳转 TMDB、豆瓣相关页面**
|
||||
- [x] **支持通过影视发现页面浏览豆瓣热门影视榜单、快速创建任务(支持智能填充任务配置)**
|
||||
|
||||
@ -68,7 +71,7 @@
|
||||
- [x] 支持多个通知推送渠道 <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/通知设置)</sup>
|
||||
- [x] 支持多账号(多账号签到、**文件管理**,仅首账号转存)
|
||||
- [x] 支持网盘文件下载、strm 文件生成等功能 <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/插件设置)</sup>
|
||||
- [x] **支持通过追剧日历功能了解订阅内容的播出情况**
|
||||
- [x] **支持通过追剧日历功能了解订阅内容的播出情况和对应文件的转存情况**
|
||||
|
||||
## 部署
|
||||
### Docker 部署
|
||||
@ -110,6 +113,7 @@ services:
|
||||
| `WEBUI_PASSWORD` | 密码 |
|
||||
| `PLUGIN_FLAGS` | 插件标志,如使用 `-emby,-aria2` 来禁用某些插件 |
|
||||
| `PORT` | 端口,Host 模式可使用此变量更换端口 |
|
||||
| `DEBUG` | 调试模式开关,可使用 `true` 或 `false` 来开启或关闭,调试模式将输出更详细的日志信息 |
|
||||
|
||||
### 青龙部署
|
||||
程序也支持以青龙定时任务的方式运行,但该方式无法使用 WebUI 管理任务,需手动修改配置文件。
|
||||
|
||||
2178
app/run.py
2178
app/run.py
File diff suppressed because it is too large
Load Diff
309
app/sdk/db.py
309
app/sdk/db.py
@ -361,15 +361,37 @@ class CalendarDB:
|
||||
)
|
||||
''')
|
||||
|
||||
# 检查 content_type 字段是否存在,如果不存在则添加
|
||||
# 检查 shows 表的新增字段(兼容旧版本)
|
||||
cursor.execute("PRAGMA table_info(shows)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
# 内容类型
|
||||
if 'content_type' not in columns:
|
||||
cursor.execute('ALTER TABLE shows ADD COLUMN content_type TEXT')
|
||||
|
||||
# 检查 is_custom_poster 字段是否存在,如果不存在则添加
|
||||
columns.append('content_type')
|
||||
# 自定义海报标记
|
||||
if 'is_custom_poster' not in columns:
|
||||
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
|
||||
cursor.execute('''
|
||||
@ -400,6 +422,21 @@ class CalendarDB:
|
||||
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(缓存:每季的三项计数)
|
||||
cursor.execute('''
|
||||
@ -599,11 +636,22 @@ class CalendarDB:
|
||||
''', (tmdb_id, season_number, episode_number, name, overview, air_date, runtime, ep_type, updated_at))
|
||||
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)
|
||||
def list_latest_season_episodes(self, tmdb_id:int, latest_season:int):
|
||||
cursor = self.conn.cursor()
|
||||
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
|
||||
WHERE tmdb_id=? AND season_number=?
|
||||
ORDER BY episode_number ASC
|
||||
@ -617,6 +665,7 @@ class CalendarDB:
|
||||
'air_date': r[3],
|
||||
'runtime': r[4],
|
||||
'type': r[5],
|
||||
'air_date_local': r[6] if len(r) > 6 else None,
|
||||
} for r in rows
|
||||
]
|
||||
|
||||
@ -642,7 +691,7 @@ class CalendarDB:
|
||||
result.append(item)
|
||||
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)
|
||||
def cleanup_orphan_data(self, valid_task_pairs, valid_task_names):
|
||||
"""清理不再与任何任务对应的数据
|
||||
@ -655,6 +704,7 @@ class CalendarDB:
|
||||
- task_metrics: 删除 task_name 不在当前任务列表中的记录
|
||||
- seasons/episodes: 仅保留出现在 valid_task_pairs 内的季与对应所有集;其余删除
|
||||
- season_metrics: 仅保留出现在 valid_task_pairs 内的记录;其余删除
|
||||
- shows: 仅保留出现在 valid_task_pairs 内的 tmdb_id;其余删除(连带删除对应的 seasons/episodes)
|
||||
"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
@ -719,6 +769,47 @@ class CalendarDB:
|
||||
except Exception:
|
||||
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()
|
||||
return True
|
||||
except Exception:
|
||||
@ -742,6 +833,214 @@ class CalendarDB:
|
||||
row = cursor.fetchone()
|
||||
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.time,HH: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)
|
||||
def get_shows_by_content_type(self, content_type:str):
|
||||
"""根据内容类型获取节目列表"""
|
||||
|
||||
@ -128,6 +128,20 @@ class TMDBService:
|
||||
return result['results'][0]
|
||||
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]:
|
||||
"""获取电视剧详细信息"""
|
||||
return self._make_request(f'/tv/{tv_id}')
|
||||
|
||||
189
app/sdk/trakt_service.py
Normal file
189
app/sdk/trakt_service.py
Normal 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
|
||||
|
||||
|
||||
@ -14,12 +14,13 @@
|
||||
--focus-border-color: #0D53FF; /* 输入框聚焦时的边框颜色 */
|
||||
--shadow-spread: 0; /* 统一阴影扩散距离设为0 */
|
||||
--button-gray-background-color: #ededf0; /* 按钮灰色背景颜色 */
|
||||
--modal-border-radius: 12px; /* 模态框弹窗和登录模块的统一圆角 */
|
||||
}
|
||||
|
||||
/* --------------- 基础样式 --------------- */
|
||||
body {
|
||||
font-size: 1rem;
|
||||
padding-bottom: 110px;
|
||||
padding-bottom: 15px;
|
||||
color: var(--dark-text-color);
|
||||
}
|
||||
|
||||
@ -188,8 +189,13 @@ main .row.title {
|
||||
margin-left: -15px; /* 添加负边距向左移动2px */
|
||||
}
|
||||
|
||||
/* 精准调整:QASX API 模块与上方模块的垂直间距减小 8px */
|
||||
main .row.title[title^="QASX API"] {
|
||||
/* 精准调整:性能设置模块与上方模块的垂直间距减小 8px */
|
||||
main .row.title[title*="配置文件加载、数据缓存和自动刷新的关键参数"] {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 精准调整:API 模块与上方模块的垂直间距减小 8px */
|
||||
main .row.title[title*="配置与任务处理、节目元数据获取及外部服务交互相关的API访问凭证"] {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@ -202,6 +208,7 @@ main .row.title h2 {
|
||||
margin-top: 0;
|
||||
line-height: 1.5;
|
||||
padding-left: 0px; /* 标题文字左内边距 */
|
||||
cursor: default; /* 不可操作的模块大标题使用普通指针 */
|
||||
}
|
||||
|
||||
/* 标题旁边的问号图标容器样式 */
|
||||
@ -1104,6 +1111,7 @@ select.form-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer; /* 可点击的图标按钮使用指针 */
|
||||
}
|
||||
|
||||
.input-group-text:has(.bi-google):hover,
|
||||
@ -1166,6 +1174,7 @@ textarea.form-control {
|
||||
border-color: var(--border-color) !important; /* 使用变量替代硬编码颜色 */
|
||||
background-color: #ededf0!important; /* 修改背景色为更浅的灰色 */
|
||||
border-width: 1px !important; /* 确保边框宽度为1px */
|
||||
cursor: default; /* 不可操作的配置选项标题使用普通指针 */
|
||||
}
|
||||
|
||||
.input-group-prepend .input-group-text {
|
||||
@ -1253,6 +1262,7 @@ table.table thead th {
|
||||
height: 40px !important; /* 确保表头高度为40px */
|
||||
line-height: 24px !important; /* 设置行高以确保文字垂直居中 */
|
||||
box-sizing: border-box !important; /* 确保边框包含在总高度内 */
|
||||
cursor: default; /* 不可操作的表头使用普通指针 */
|
||||
}
|
||||
|
||||
/* 表头悬停样式 */
|
||||
@ -1457,7 +1467,7 @@ button.close:focus,
|
||||
}
|
||||
|
||||
#logModal .modal-content {
|
||||
border-radius: 6px;
|
||||
border-radius: var(--modal-border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@ -1467,8 +1477,8 @@ button.close:focus,
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 11px 16px;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-top-left-radius: var(--modal-border-radius);
|
||||
border-top-right-radius: var(--modal-border-radius);
|
||||
}
|
||||
|
||||
#logModal .modal-title {
|
||||
@ -1477,6 +1487,7 @@ button.close:focus,
|
||||
color: var(--dark-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default; /* 不可操作的模态框标题使用普通指针 */
|
||||
}
|
||||
|
||||
#logModal .modal-title b {
|
||||
@ -1546,7 +1557,7 @@ button.close:focus,
|
||||
#fileSelectModal .modal-content,
|
||||
#createTaskModal .modal-content,
|
||||
#editMetadataModal .modal-content {
|
||||
border-radius: 6px;
|
||||
border-radius: var(--modal-border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@ -1558,8 +1569,8 @@ button.close:focus,
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 11px 16px;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-top-left-radius: var(--modal-border-radius);
|
||||
border-top-right-radius: var(--modal-border-radius);
|
||||
}
|
||||
|
||||
#fileSelectModal .modal-title,
|
||||
@ -1570,6 +1581,7 @@ button.close:focus,
|
||||
color: var(--dark-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default; /* 不可操作的模态框标题使用普通指针 */
|
||||
}
|
||||
|
||||
#fileSelectModal .modal-title b,
|
||||
@ -1663,6 +1675,7 @@ button.close:focus,
|
||||
/* 编辑元数据模态框:输入前标题灰底,统一输入高度与字体,与创建任务保持一致 */
|
||||
#editMetadataModal .input-group-prepend .input-group-text {
|
||||
background-color: var(--button-gray-background-color) !important;
|
||||
cursor: default; /* 不可操作的前缀文本使用普通指针 */
|
||||
}
|
||||
#editMetadataModal .form-control,
|
||||
#editMetadataModal .input-group-text,
|
||||
@ -1839,6 +1852,7 @@ button.close:focus,
|
||||
z-index: 5;
|
||||
vertical-align: middle; /* 添加:垂直居中对齐 */
|
||||
height: 40px !important; /* 添加:自动高度,确保与内容一致 */
|
||||
cursor: default; /* 不可操作的表头使用普通指针 */
|
||||
}
|
||||
|
||||
/* 模态框表格列宽设置 - 基于内容类型 */
|
||||
@ -2253,7 +2267,7 @@ div.jsoneditor-tree button.jsoneditor-button:focus {
|
||||
.login-card {
|
||||
width: 340px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--modal-border-radius);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
@ -2394,7 +2408,7 @@ div.jsoneditor-tree button.jsoneditor-button:focus {
|
||||
transition: color 0.2s; /* 添加颜色过渡效果 */
|
||||
}
|
||||
|
||||
/* 侧边栏菜单项图标样式 */
|
||||
/* 侧边栏菜导航图标样式 */
|
||||
.sidebar .nav-link .bi-list-ul {
|
||||
font-size: 1.1rem;
|
||||
position: relative;
|
||||
@ -2423,8 +2437,12 @@ div.jsoneditor-tree button.jsoneditor-button:focus {
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .bi-terminal {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .bi-power {
|
||||
font-size: 1.27rem;
|
||||
font-size: 1.26rem;
|
||||
}
|
||||
|
||||
.bottom-links .nav-link .bi-book {
|
||||
@ -2724,6 +2742,11 @@ body {
|
||||
color: var(--dark-text-color);
|
||||
}
|
||||
|
||||
/* 所有模态框标题使用普通指针 */
|
||||
.modal-title {
|
||||
cursor: default; /* 不可操作的模态框标题使用普通指针 */
|
||||
}
|
||||
|
||||
/* 设置输入框占位符的颜色 */
|
||||
.form-control::placeholder {
|
||||
color: var(--light-text-color); /* 修改占位符颜色 */
|
||||
@ -4913,6 +4936,17 @@ select.task-filter-select,
|
||||
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 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -5227,6 +5261,7 @@ table.selectable-files th {
|
||||
height: 40px !important; /* 确保表头高度为40px */
|
||||
line-height: 24px !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;
|
||||
}
|
||||
|
||||
/* 文件选择模态框的取消按钮样式(通用样式,覆盖所有情况) */
|
||||
#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 {
|
||||
@ -6007,6 +6055,7 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer; /* 可点击的按钮使用指针 */
|
||||
}
|
||||
|
||||
#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;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--dark-text-color) !important;
|
||||
cursor: default; /* 不可操作的后缀文本使用普通指针 */
|
||||
}
|
||||
|
||||
#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;
|
||||
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-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 {
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
@ -6530,10 +6597,10 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
|
||||
/* 使用多向阴影模拟加粗(图标为字体,font-weight不生效) */
|
||||
text-shadow:
|
||||
0 0 0 currentColor,
|
||||
0 0.45px 0 currentColor,
|
||||
0 -0.45px 0 currentColor,
|
||||
0.45px 0 0 currentColor,
|
||||
-0.45px 0 0 currentColor;
|
||||
0 0.3px 0 currentColor,
|
||||
0 -0.3px 0 currentColor,
|
||||
0.3px 0 0 currentColor,
|
||||
-0.3px 0 0 currentColor;
|
||||
}
|
||||
|
||||
.discovery-create-task {
|
||||
@ -6744,6 +6811,7 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: default; /* 不可操作的类型/集数信息使用普通指针 */
|
||||
}
|
||||
|
||||
.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"] {
|
||||
position: relative !important;
|
||||
@ -7025,6 +7099,11 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
.calendar-controls .btn-group {
|
||||
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;
|
||||
font-size: 0.95rem;
|
||||
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);
|
||||
border-color: var(--focus-border-color);
|
||||
color: white;
|
||||
@ -7302,12 +7388,13 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
font-size: 0.95rem;
|
||||
color: var(--dark-text-color);
|
||||
border-right: 1px solid var(--border-color); /* 显示内部分割线 */
|
||||
background-color: #f7f7f9;
|
||||
background-color: var(--button-gray-background-color);
|
||||
/* 防止文本被挤压成竖排 */
|
||||
min-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: default; /* 不可操作的星期表头使用普通指针 */
|
||||
}
|
||||
|
||||
/* 移除星期导航最后一列的右侧分割线,避免溢出 */
|
||||
@ -7386,6 +7473,13 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
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 {
|
||||
background-color: transparent !important; /* 去除有播出集背景色 */
|
||||
}
|
||||
@ -7402,7 +7496,7 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px; /* 等距6px */
|
||||
line-height: 1; /* 统一行高,消除上下视觉不等距 */
|
||||
/* 日历视图:默认保持原按钮灰背景 */
|
||||
/* 日历视图:默认保持原按钮灰背景(未转存且未播出) */
|
||||
background-color: #f7f7f9;
|
||||
border-radius: 6px; /* 圆角6px */
|
||||
display: flex;
|
||||
@ -7410,11 +7504,16 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 日历视图:仅当该集已达转存进度时,卡片背景改为导航悬停浅蓝色 */
|
||||
/* 日历视图:当该集已达转存进度时,卡片背景改为导航悬停浅蓝色 */
|
||||
.calendar-month-episode:has(.episode-number.episode-number-reached) {
|
||||
background-color: #e6f1ff;
|
||||
}
|
||||
|
||||
/* 日历视图:未转存但已播出的集,卡片背景为浅绿色 */
|
||||
.calendar-month-episode:has(.episode-number.episode-number-aired):not(:has(.episode-number.episode-number-reached)) {
|
||||
background-color: #E6F7EE;
|
||||
}
|
||||
|
||||
.episode-title {
|
||||
font-weight: 400; /* 常规体 */
|
||||
flex: 1;
|
||||
@ -7584,11 +7683,14 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
}
|
||||
/* 超小模式:表格内部的卡片背景色覆盖(仅表格体内紧凑卡片) */
|
||||
.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) {
|
||||
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;
|
||||
line-height: 1;
|
||||
transform: none; /* 还原不上移 */
|
||||
background-color: #f7f7f9; /* 桌面端默认背景 */
|
||||
background-color: #f7f7f9; /* 桌面端默认背景:未转存且未播出 */
|
||||
border-radius: 6px;
|
||||
}
|
||||
.calendar-selected-episodes .calendar-month-episode:has(.episode-number.episode-number-reached) {
|
||||
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) */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 显示设置:拖拽时显示“移动”而非“添加”视觉提示 */
|
||||
/* 显示设置:拖拽时显示"移动"而非"添加"视觉提示 */
|
||||
.draggable-item {
|
||||
cursor: move; /* 显示移动光标 */
|
||||
}
|
||||
|
||||
/* 显示设置可拖动配置项的标题也使用拖拽指针 */
|
||||
.draggable-item .input-group-text {
|
||||
cursor: move !important; /* 可拖动配置项的标题使用拖拽指针 */
|
||||
}
|
||||
|
||||
.draggable-item:active {
|
||||
opacity: 1;
|
||||
}
|
||||
@ -7894,10 +8004,16 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* QASX API Token 显示框背景色:与普通输入框保持一致,覆盖 disabled 状态的灰色背景 */
|
||||
.form-control.token-display:disabled,
|
||||
.form-control.token-display[readonly] {
|
||||
background-color: #fff !important; /* 与普通输入框背景色保持一致 */
|
||||
}
|
||||
|
||||
/* TMDB 说明文本样式与链接样式(继承颜色、无下划线、悬停不变) */
|
||||
.tmdb-attribution {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
.tmdb-attribution a {
|
||||
@ -7907,7 +8023,7 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
.tmdb-attribution a:hover,
|
||||
.tmdb-attribution a:focus {
|
||||
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; /* 与左侧类型按钮一致 */
|
||||
}
|
||||
|
||||
/* 左侧“按”按钮:保留边框,去右边框,与中间边框重叠 */
|
||||
/* 左侧"按"按钮:保留边框,去右边框,与中间边框重叠 */
|
||||
.tasklist-sort-pill-icon {
|
||||
width: 31px;
|
||||
min-width: 31px;
|
||||
@ -7950,6 +8066,7 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
border-right: none !important;
|
||||
border-radius: 6px 0 0 6px;
|
||||
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;
|
||||
-moz-appearance: none;
|
||||
background: #fff !important;
|
||||
cursor: pointer;
|
||||
cursor: default;
|
||||
background-image: none;
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
@ -8409,10 +8526,131 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
|
||||
}
|
||||
|
||||
.tasklist-count-number {
|
||||
cursor: text;
|
||||
cursor: default;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* 追剧日历计数模块:减少左侧与排序组件的间距8px(ml-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 {
|
||||
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
@ -9,10 +9,6 @@ import re
|
||||
import os
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TaskExtractor:
|
||||
def __init__(self):
|
||||
# 剧集编号提取模式
|
||||
@ -233,31 +229,19 @@ class TaskExtractor:
|
||||
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 = []
|
||||
|
||||
for i, task in enumerate(tasks):
|
||||
try:
|
||||
logging.debug(f"处理第{i+1}个任务: {task.get('taskname', '')}")
|
||||
|
||||
task_name = task.get('taskname', '')
|
||||
save_path = task.get('savepath', '')
|
||||
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)
|
||||
logging.debug(f"show_info: {show_info}")
|
||||
|
||||
# 提取进度信息
|
||||
progress_info = self.extract_progress_from_latest_file(latest_file)
|
||||
logging.debug(f"progress_info: {progress_info}")
|
||||
|
||||
# 优先使用任务显式类型(配置或提取出的),否则回退到路径判断
|
||||
explicit_type = None
|
||||
@ -283,16 +267,11 @@ class TaskExtractor:
|
||||
'progress_type': progress_info.get('progress_type')
|
||||
}
|
||||
|
||||
logging.debug(f"task_info: {task_info}")
|
||||
tasks_info.append(task_info)
|
||||
|
||||
except Exception as e:
|
||||
logging.debug(f"处理任务 {i+1} 时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
logging.debug(f"TaskExtractor.extract_all_tasks_info 完成,返回任务数量: {len(tasks_info)}")
|
||||
return tasks_info
|
||||
|
||||
def get_content_type_display_name(self, content_type: str) -> str:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user