feat(adapters): qBittorrent, filesystem, TMDB and Emby adapters with testkit mocks (W1-3a)
- qbittorrent: WebUI API v2 client (cookie/API-key auth, add/info/files/filePrio, pause/resume with 5.x stop/start fallback, tags, delete with explicit deleteFiles, maindata), error classes, idempotent submit with local infohash (own bencode) and post-timeout hash/probe-tag lookup; state mapping per MoviePilot - fs: copyWithVerify (tmp + fsync + size/sha256/sample + no-clobber commit), moveSafe (EXDEV surfaced), resumable crossDeviceMove, statSafe/checkRoot with root sentinel, budgeted incremental scan with cursor, ffprobe, statfs usage - tmdb: multi-language search/details/seasons, LRU+TTL cache with coalescing, rate-limit backoff, MoviePilot match chain (name normalization, year +-1) - emby: Emby/Jellyfin items/seasons/episodes/MediaSources, failures -> unknown - testkit: mock qB / TMDB / Emby servers, torrent builder, temp-tree fixtures - docs: W1-3a porting table appended to downloader-and-sites ledger Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
9c881e737f
commit
ef2e504a94
45 files changed
+5766
-1
No files matched your search
@@ -545,3 +545,66 @@ MC 注册表 `MC:src/movieclaw_tracker/registry.py:172` `_load_site_yaml` 读取
|
||||
| 观众促销/H&R 真实类名 | MC 未配置,需用真实页面(或用户提供的脱敏 HTML)核对后写入 `audiences.ts` |
|
||||
| M-Team `genDlToken` 返回直链的有效期与频率限制 | 两项目均未写明;mock 中按一次性处理 |
|
||||
| qB 5.x API Key 认证与 `added_torrent_ids` 字段的最小版本 | MP 以运行时探测兼容;需在 spike 中以 qB 4.6.x / 5.1.x 实例验证 |
|
||||
|
||||
---
|
||||
|
||||
## W1-3a 实际迁移对照(qB / TMDB / Emby / 文件系统)
|
||||
|
||||
上游:`/opt/moviepilot-plus/upstream/MoviePilot`,提交 `e6820a8`。只做行为对照与 TS 重写,不复制代码。
|
||||
|
||||
### qBittorrent(`packages/adapters/src/qbittorrent/`)
|
||||
|
||||
| 上游函数 | TS | 保留 / 调整 |
|
||||
| --- | --- | --- |
|
||||
| `Qbittorrent.__init__` / `__login_qbittorrent` / `is_inactive` / `reconnect` | `QbClient` 构造 + `login()`;401/403 时自动重登一次 | 保留 API Key(Bearer)与账号密码两种认证;**不**调用 `__sync_incomplete_file_suffix`(QA6,只读 `preferences()`) |
|
||||
| `__parse_add_torrent_response` | `add()` 响应解析 | 保留:文本 `Ok.` 或 5.x JSON(`success_count/pending_count/added_torrent_ids`) |
|
||||
| `add_torrent` | `add()` | 保留 savepath/category/tags/paused(同时发 `stopped`)/rename/sequential/firstLastPiece(实例默认);调整:`autoTMM` 固定 false、显式 `contentLayout`(QA7);Cookie 同步(Q14)未迁移——.torrent 由站点适配器下载后以字节提交 |
|
||||
| `get_torrents` | `list({hashes, tag, tags, category, filter})` | 调整:首个标签走 qB 原生 `tag` 参数,其余在客户端做子集过滤(QA4);按 hash 查询不受管理标签限制(QA3) |
|
||||
| `get_completed_torrents` | `getCompleted()` + `isDownloadComplete()` | 保留严格完成条件与非终态集合(QK4);未知 state 也不算完成 |
|
||||
| `get_downloading_torrents` | `getDownloading()` | 保留 |
|
||||
| `__normalize_torrent_state`(`__init__.py`) | `mapQbState()` | 调整:error/missingFiles/unknown 独立状态,不兜底为 completed(QA5) |
|
||||
| `delete_torrents_tag` / `remove_torrents_tag` / `set_torrents_tag` | `deleteTorrentsTag()` / `removeTags()` / `addTags()`;`deleteTagDefinitions()` | 保留 |
|
||||
| `torrents_set_force_start` | `setForceStart()` | 保留;是否调用由上层按实例 `force_resume` 配置决定 |
|
||||
| `get_torrent_id_by_tag` / `__get_last_add_torrentid_by_tag` | `submit()` 内探针标签轮询 | 保留 9 次 × 3s 默认(首查不等待)、用后删标签与全局定义;调整:探针前缀 `uhd-probe-`(QA13),hash 已知时不用探针(QK2) |
|
||||
| `start_torrents` / `stop_torrents` | `resume()` / `pause()` | 5.x 端点改名(stop/start)时自动切换 |
|
||||
| `delete_torrents` | `delete(hashes, { deleteFiles })` | 调整:`deleteFiles` 必须显式,删数据前的引用检查在上层(QA11/QA12) |
|
||||
| `get_files` | `files(hash, { retries, intervalMs })` | 保留:空列表视为未就绪继续重试 |
|
||||
| `set_files` | `setFilePriority()` | 保留:无 hash/无文件 ID 返回 false 不发请求 |
|
||||
| `recheck_torrents` / `set_torrent_location` / `set_torrent_category` / `change_torrent` | `recheck()` / `setLocation()` / `setCategory()` / `changeTorrent()` | 保留 KB/s→B/s 换算、仅做种时长时 ratio=-2 |
|
||||
| `get_content_layout` / `transfer_info` | `contentLayout()` / `transferInfo()` | 只读 |
|
||||
| `QbittorrentModule.download` 步骤 1-7 | `submit()` | 调整:本地 infohash 预查已存在 → `already_exists` 不改其路径/标签(QA1/QA2);add 超时/断连 → 按 hash 回查(QA8),返回判别联合(QA14)。选集(步骤 8-9)属 domain `selectTorrentFiles`,本适配器只提供 `files`/`setFilePriority` |
|
||||
| `_get_torrent_info`(`_base/downloader.py`) | `parseTorrent()` / `parseMagnet()` | 新增 v1/v2 infohash(原始 info 字节切片求哈希) |
|
||||
| `normalize_path` / `normalize_return_path` | `toDownloaderPath()` / `fromDownloaderPath()` | 保留首条命中 |
|
||||
|
||||
### TMDB(`packages/adapters/src/tmdb/`)
|
||||
|
||||
| 上游函数 | TS | 保留 / 调整 |
|
||||
| --- | --- | --- |
|
||||
| `search_movies` / `search_tvs` / `search_multiis` | `search()`(多语言合并)/ `searchRaw()`(首选语言)/ `searchMulti()` | 电影年份参数 `year`,剧集 `first_air_date_year`;multi 只保留 movie/tv |
|
||||
| `__compare_names` + `foundation/text.py remove_punctuation` | `compareNames()` / `normalizeTitleForCompare()` | 保留同一标点集、去零宽字符与空白、大写后**完全相等** |
|
||||
| `_generate_year_range` | `yearRange()` | 保留 [year, year+1, year-1] |
|
||||
| `match` / `_build_match_plan` / `_match_resolution` | `matchTmdb()` | 保留步骤顺序(季年份优先,再名称+年份容差)与首个命中短路;调整:查询异常抛 `TmdbError`,不当作"未找到"继续 |
|
||||
| `__search_movie_by_name` / `__search_tv_by_name` / `_build_name_candidate_plan` / `_name_resolution` / `_alias_matches` / `__get_names` | `matchTmdb()` 内 `searchByName` | 保留:按日期降序、年份前缀过滤、标题/原标题直达,否则逐个拉详情用别名(alternative_titles)+ 译名(translations)比较 |
|
||||
| `__search_tv_by_season` / `_season_candidate_matches` / `_season_year_matches` | `matchTmdb()` 内 `searchBySeason` | 保留;剧集组 `group_seasons` 未迁移 |
|
||||
| `match_multi` / `_sort_multi_results` | `matchTmdbMulti()` | 保留电影优先、日期降序 |
|
||||
| `__get_movie_detail` / `__get_tv_detail` | `details()` | 附带 `alternative_titles,external_ids,translations`;输出归一化 `MetadataCandidate` |
|
||||
| `get_tv_season_detail` | `season()` | 保留 |
|
||||
| `tmdbv3api/tmdb.py request` 的 `@cached` | `TtlLruCache` + 并发合并 | 保留 maxsize 1024 / TTL 24h(可配)/ 空结果 30min / 失败不缓存;新增并发合并与 429 Retry-After 退避 |
|
||||
| `cache.py TmdbCache`(识别结果持久缓存) | 未迁移 | 识别结果持久化归 db/domain(`external_identity`) |
|
||||
|
||||
### Emby / Jellyfin(`packages/adapters/src/emby/`)
|
||||
|
||||
| 上游函数 | TS | 保留 / 调整 |
|
||||
| --- | --- | --- |
|
||||
| `Emby.get_user` | `resolveUserId()` | 保留:用户名匹配 → 管理员回退 |
|
||||
| `__get_emby_series_id_by_name` | `findSeriesIdByName()` | 保留 SearchTerm + 名称完全相等 + 年份;调整:连不通 → `unknown`,未找到 → `ok(null)` |
|
||||
| `get_movies` | `getMovies()` | 保留名称完全相等 + 年份 + 身份兼容 |
|
||||
| `get_tv_episodes` | `getTvEpisodes()` | 保留 `IsMissing=false`、按 ParentIndexNumber/IndexNumber 汇总、缓存 itemId 失效时按标题重查;新增 Virtual 过滤与多集文件(IndexNumberEnd)展开 |
|
||||
| `get_iteminfo` | `getItem()` | 单条目 404 → `ok(null)`(ID 失效),其他失败 → `unknown` |
|
||||
| `MediaServerIdentityHelper.from_provider_ids` / `are_compatible` | `providerIdentity()` / `identitiesCompatible()` | 保留来源优先级与"同来源不同 ID 才冲突" |
|
||||
| (新增) | `searchItems()` / `findByProviderId()` / `seasons()` / `episodes()` / `normalizeMediaSource()` | MediaSources 版本信息;查询截断时不给"不存在" |
|
||||
| `jellyfin.py` 认证 | `flavor: "jellyfin"` | 保留 `Authorization: MediaBrowser Token=`、无 `/emby` 前缀;调整:Emby 也用请求头 `X-Emby-Token`,不把 api_key 放进 URL |
|
||||
|
||||
### 文件系统(`packages/adapters/src/fs/`)
|
||||
|
||||
对照 `docs/porting/moviepilot-transfer.md` §2.6:`_write_atomically`/`_partial_path`/`_cleanup_stale_partials` → `copyWithVerify`/`partialPathFor`/`sweepStalePartials`(新增 fsync、校验、no-clobber);`LocalStorage.move` → `moveSafe`(EXDEV 不降级)+ `crossDeviceMove`(三步可重入)。`statSafe`/`scanIncremental`/`ffprobe`/`diskUsage` 为本项目新增。
|
||||
Reference in new issue
Block a user