# funflix **Repository Path**: farfarfun/funflix ## Basic Information - **Project Name**: funflix - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-31 - **Last Updated**: 2026-10-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # funflix 影视资源分享文本的结构化采集、解析与网盘链接校验。 完整设计见 [docs/DESIGN.md](docs/DESIGN.md),已知待优化项见 [docs/TODO.md](docs/TODO.md), 给本项目贡献代码见 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)。 ## 流水线 ``` source ──采集──> raw_document ──LLM 抽取──> extraction (频道/水位) (原始文本) │ ▼ work → media + resource ──校验──> link_check (剧) (季) (网盘链接) ``` 每一层各自幂等、可单独重跑。当前进度: | 阶段 | 状态 | | --- | --- | | M0 采集源 + Telegram / RSS 采集器 + 水位 | ✅ 已完成 | | M1 数据模型 + 迁移 + 原始文本接口 | ✅ 已完成 | | M2 链接扫描 + 文本分段 + 剧名归一 | ✅ 已完成 | | M3 LLM 抽取 | ✅ 已完成 | | M4 网盘校验(夸克 / UC / 阿里 / 123 / 城通,匿名探针) | ✅ 已完成 | | M5 worker 租约 + 常驻调度 | ✅ 已完成 | | M6 查询接口 | ✅ 已完成(SQLite 走 LIKE,FTS5 后端待补) | | M7 其余网盘探针(百度 / 蓝奏 / 天翼等) | 待开发 | ### 当前采集源 | 类型 | 当前实现 | 增量依据 | 适合接入 | | --- | --- | --- | --- | | `telegram` | Telegram 公开 Web 预览页 | 消息 ID,可追新/补历史 | 频道分享文本 | | `tencent_docs` | 腾讯智能表格接口 | sheet 版本与行偏移 | 结构化资源表 | | `tencent_doc` | 腾讯文本文档接口 | 文档 revision | 文本型资源清单 | | `kdocs` | 金山多维表格接口 | sheet 偏移 | 结构化资源表 | | `rss` | 通用 RSS 2.0 / Atom | 条目 `guid` / `id` | 公开影视、动漫、种子 feed | | `forum` | 集合页 | 详情页 ID,可追新/补历史 | 一个索引页下的多资源合集 | | `web` | 公开影视网站与论坛 | 详情页 URL,可持续追新 | 网盘、磁力与 torrent 附件 | | `api` | 人人影视快照与评论接口 | 快照校验和 / 评论 ID | 历史资源与网友分享 | `weibo` 等枚举值目前没有专用采集器,普通公开 URL 会由 `web` 兜底。 ## 快速开始 ```bash pip install funflix # 或 uv tool install funflix # 建库 funflix db upgrade # 登记一个 Telegram 频道并采集一次 funflix source add https://t.me/s/<频道名> funflix source collect funflix source list ``` HTTP API 服务由独立的 `funflix-api` 包提供(依赖本包),见 [funflix-api](https://github.com/farfarfun/funflix-api)。 ## 命令行 | 命令 | 执行的环节 | | --- | --- | | `funflix collect` | 采集 | | `funflix parse` | 解析,默认处理到清空为止,可用 `--limit` 显式设上限 | | `funflix verify` | 校验,默认处理到清空为止,可用 `--limit` 显式设上限 | | `funflix run` | 采集 → 解析(`--skip-collect` 时只解析,均不含校验),解析默认处理到清空为止 | | `funflix worker --once` | 采集 → 解析 → 校验,各推进到队列清空后退出 | | `funflix worker` / `funflix-api start`/`run`(`FUNFLIX_WORKER_ENABLED=true`) | 循环反复:采集 → 解析 → 校验,各推进到队列清空,直到停止(某一队列大量积压时会在这一轮里暂时独占,属预期行为) | `parse`/`verify` 内部按 `--batch-size`(默认 500)分批拉取执行,不会一次性把全部 待处理行读进内存;进度条从一开始就按总量显示,过程中持续推进。每批内部再按 `--concurrency` 个并发任务处理,每个任务用独立数据库连接与事务(提交粒度因此 是"每条一提交")。`parse` 默认 `--concurrency 20`,`verify` 默认 `--concurrency 8`—— 两者默认值不同是因为风险不对称:`parse` 的瓶颈纯粹是远程数据库往返延迟,多开 并发没有副作用,上限只需留在数据库连接池容量(`pool_size=10` + `max_overflow=20` = 30)以内;`verify` 的并发受限于对网盘的探测频率(见下方限速取舍),盲目调高 没有额外收益。`parse` 在并发下,两条文档若抽出同一部作品会撞上 `Media`/`Resource`/`Tag` 的唯一约束,此时按"被并发抢跑"静默回滚重试、不计入 `parse_attempts`,留到下次 `funflix parse` 自然捞到,不会被误判成真正失败; `verify` 没有这个问题(按已知 `resource.id` 操作,不存在先查后建)。 `verify` 的默认 `--rate` 已从 1/秒提到 5/秒——见下方「网盘校验」一节的取舍说明。 以上命令都会每隔 `FUNFLIX_WORKER_PROGRESS_SECONDS`(默认 5,`<=0` 关闭)秒打一行 `采集[待处理/处理中/已完成] 解析[...] 校验[...]` 心跳,内容是**数据库里全局的三阶段快照**, 与当前命令具体在跑哪个环节无关;命令自身的处理进度仍看 tqdm 进度条 (`采集 N/total 源`、`解析 N/total 条` 等)。 `db reset` / `db retag` / `db requeue`、`ingest` 是一次性批量操作,没有逐条推进的过程,不在此列。 ### 全部命令 顶层命令: | 命令 | 说明 | | --- | --- | | `funflix status` | 查看流水线各环节的记录数(`--verbose` 展开采集源明细) | | `funflix collect [source_id]` | 采集:把源里的新内容写成原始文本;不传 `source_id` 则采集全部启用的源 | | `funflix parse` | 抽取:把原始文本解析成作品与资源 | | `funflix verify` | 校验:探测网盘链接现在还能不能用 | | `funflix run` | 一条龙:采集全部启用的源,再解析待处理文本 | | `funflix worker` | 常驻后台 worker:周期性地采集、解析、校验(`--once` 只跑一轮就退出) | | `funflix probes` | 列出可用的网盘校验探针 | | `funflix extractors` | 列出可用的抽取器 | | `funflix search ` | 按剧名搜索作品,一部剧一条、季是子层(`--links` 调每季列几条链接) | | `funflix doc ` | 查看一条原始文本及其解析状态 | | `funflix ingest ` | 从文件导入原始文本(`.txt` / `.jsonl`) | 后台 worker 也可以通过 `scripts/setup.sh` 管理(`` 在前,`` 在后; `status` 不区分环境,不带参数): ```bash scripts/setup.sh start dev # 后台启动(dev 走 uv run,跑工作树里的源码) scripts/setup.sh status # 不区分环境;未运行时返回非 0 scripts/setup.sh stop dev scripts/setup.sh restart dev scripts/setup.sh run prod # 前台运行(prod 跑已安装的 funflix 正式包) ``` `dev` 用 `uv run funflix worker`,`prod` 用 PATH 里已安装的 `funflix` 正式包; `prod` 下找不到 `funflix` 命令会直接报错退出,不会回退到源码。`start` 会等待 约 2 秒(`FUNFLIX_START_WAIT` 可调)确认进程确实活着才写 PID 文件并报告成功, 启动即退出会打印日志尾部并返回非 0;对已经在运行的 worker 重复 `start` 也返回非 0。 运行状态、PID 和日志位于 `.run/`。 ### 持续修复:规则更新后把旧数据刷对 解析规则永远加不完。每往 `normalize.py` 补一条词表,库里就多出一批「按旧规则 算出来、现在看是错的」数据。所以这不是一次性迁移,而是三条可以反复跑的命令 (`funflix repair`,和 `funflix canon` 一样**默认 dry-run**,加 `--apply` 才写库): | 命令 | 性质 | | --- | --- | | `funflix repair scan` | 只读 media + 只写 `repair_task`。规则没变就零写入,可每轮跑 | | `funflix repair apply` | 破坏性、不可逆。带 `--limit` 限额和爆炸半径闸门 | | `funflix repair requeue` | 把规则版本过期的文档打回 parse 队列(深层修复) | 检测和应用分成两步是故意的:检测便宜且可逆,可以天天跑;应用不可逆 —— 两部剧并成一部之后没有任何信息能把它们分回去。`repair apply` 在动手前先查 爆炸半径,删除任务超过 media 总量 5%、或重挂超过 20% 就直接拒绝(要 `--force` 放行)—— 一次规则改动不该动到全库两成,真触发了更可能是规则写错了。 ```bash funflix repair scan # dry-run:看这轮检出多少、都是什么样 funflix repair scan --apply # 落成 repair_task funflix repair apply # dry-run:核对 改标题/多合一/删除 各几条 funflix repair apply --apply --limit 5000 # 真跑,限额 funflix repair scan --key --apply # 单组演练 funflix repair apply --key ``` 这三条已经挂在 `.github/workflows/collect.yml` 的 `repair` job 上,和 collect / parse / verify 完全并列、不设 `needs`,靠 `--limit` 慢慢刷。 浅层(scan + apply)从已清洗的 `media.title` 重算 —— 可行是因为清洗是**减法**, 对已经干净的标题是空操作。它修不了「旧规则洗坏、信息已经丢了」和「抽取器切分 逻辑本身变了」这两类,那些只能靠 `repair requeue` 回到 `raw_document.content` 重解析;改了抽取逻辑要手动 bump `PARSE_RULES_VERSION`(刻意不用源码哈希: 改个注释不该让 213 万份文档重排队)。 `funflix canon` 的四个子命令是同一套数据的 LLM 一侧:`purge` / `rebuild` 靠规则 收口,剩下的残局交给 `resolve`(写 `title_canon` 裁决)和 `merge`(应用裁决)。 `repair` 复用 `canon/lookup.py::resolve_target` 算目标值 —— 修复的终点必须和 parse 现在会产出的结果一字不差,否则两条路会互相拆台。设计取舍见 `docs/DESIGN.md` §7.5。 采集源除 Telegram 频道和腾讯文档外,还支持公开 RSS/Atom feed。RSS 条目标题、 描述、真实链接、Atom enclosure,以及扩展字段里的 `infoHash` 都会转成原始文本,按 feed 的 `guid`/`id` 增量去重。示例: ```bash funflix source add 'https://feeds.example/latest.rss' funflix source collect ``` `funflix db` 子命令: | 命令 | 说明 | | --- | --- | | `funflix db upgrade` | 执行数据库迁移 | | `funflix db downgrade ` | 回滚迁移 | | `funflix db current` | 显示当前数据库版本 | | `funflix db revision -m "..."` | 按模型变更自动生成迁移脚本 | | `funflix db reset` | 清空数据表并重建(采集源配置保留,可用 `--keep-documents`/`--keep-cursors` 调整范围) | | `funflix db retag` | 按当前规则重新归类已有标签,合并重复项 | | `funflix db requeue` | 把"新支持的网盘"的历史资源放回校验队列 | | `funflix db info` | 显示当前连接的数据库方言 | `funflix source` 子命令: | 命令 | 说明 | | --- | --- | | `funflix source add ` | 登记一个采集源,类型与标识按 URL 自动识别 | | `funflix source list` | 列出全部采集源及其水位 | | `funflix source show ` | 查看采集源详情 | | `funflix source set ` | 修改采集源配置(间隔、翻页上限、水位、标题) | | `funflix source reset-cursor [source_id]` | 只归零采集水位,已采集的原始文本原样保留;不传则重置全部源 | | `funflix source enable ` | 启用采集源 | | `funflix source disable ` | 停用采集源 | | `funflix source remove ` | 删除采集源(已采集的原始文本会保留) | | `funflix source types` | 列出当前支持的采集源类型 | | `funflix source collect [source_id]` | 立即采集一次,等价于顶层 `funflix collect` | ## 配置 所有配置项走 `FUNFLIX_` 前缀的环境变量或 `.env`: | 变量 | 默认值 | 说明 | | --- | --- | --- | | `FUNFLIX_DATABASE_URL` | `sqlite+aiosqlite:///~/.farfarfun/funflix/funflix.db` | 切 PG 改成 `postgresql+asyncpg://...`;默认是绝对路径,换目录执行读写的也是同一份库 | | `FUNFLIX_SESSION_SECRET` | 随机(每次重启变化) | 会话 cookie 签名密钥;多进程/需跨重启保留会话的部署必须固定配置 | | `FUNFLIX_SESSION_MAX_AGE` | `2592000`(30 天) | 会话 cookie 有效期(秒) | | `FUNFLIX_SESSION_COOKIE_SECURE` | `false` | 会话 cookie 是否加 Secure 标记;确认部署链路全程 HTTPS 后再打开 | | `FUNFLIX_REGISTRATION_ENABLED` | `false` | 是否开放自助注册;默认关闭,账号由 `funflix user create` 创建 | | `FUNFLIX_LOG_LEVEL` | `INFO` | | | `FUNFLIX_INGEST_MAX_BATCH` | `200` | 单批提交上限 | | `FUNFLIX_INGEST_MAX_CONTENT_LENGTH` | `100000` | 单条文本长度上限 | ## 接口 HTTP API(`/api/v1/sources`、`/raw`、`/works`、`/resources`、`/stats`、鉴权等)由 独立的 `funflix-api` 包提供,见 [funflix-api](https://github.com/farfarfun/funflix-api) 的 README。CLI(`funflix user` / `funflix source` 等)直连数据库,不走 HTTP,不受影响。 `/works` 的关键词匹配走 `services/search.py` 的后端抽象:PostgreSQL 上用 `pg_trgm` 容错匹配并按相似度排序,其余方言回落 `LIKE`,调用方无感知。搜索的主体是 `work` (一部剧)而不是 `media`(一季)—— 搜「大主宰」给的是一条「大主宰(3 季 / 78 资源)」,而不是几百条同名行。 PostgreSQL 上的三条实测结论(`tests/test_search_pg.py`,5 万行): | | 结果 | | --- | --- | | 3 字以上关键词 | 走 GIN 位图索引,**0.235ms** | | 2 个汉字的关键词 | 全表扫描 **81.9ms**,无解 —— pg_trgm 要三元组,两个字提不出完整 trigram | | 刚批量导入完 | 暂时走不了索引,见下 | > 📌 **批量导入后记得 VACUUM**。GIN 索引默认开着 fastupdate,新行先进一个待合并 > 列表;合并前规划器认为索引很贵(实测位图扫描启动代价 2515 vs 合并后 64), > 于是绕开它走全表扫描。autovacuum 会自动处理,赶时间就手动 > `VACUUM ANALYZE work;`。 > ⚠️ DESIGN §7.3 还规划了 `SqliteFtsBackend`(FTS5 虚拟表),目前**尚未实现**。 > 也就是说 SQLite 部署上关键词搜索仍是 `LIKE %x%` 全表扫描 —— 几千条无所谓, > 上万条就会明显变慢。数据量起来之前先用 PostgreSQL,或者补上 FTS5 后端。 ## 网盘校验 当前有匿名探针的网盘:**夸克、UC、阿里云盘、123 云盘、城通网盘**。其余 provider 的链接照常入库, 只是标成 `unsupported`、不做校验。 UC 与夸克是同一套接口(连业务码都一样),所以 UC 探针直接复用夸克的码表。 > ⚠️ **默认限速(`--rate`/`FUNFLIX_WORKER_VERIFY_RATE`)已从 1/秒提到 5/秒**。 > 打太快可能触发网盘风控,被限流的响应有被误判成"链接失效"、把整库资源 > 误杀的风险(见下方探针的第一原则)——这是权衡校验吞吐后接受的风险, > 不是默认安全值。观察到误判迹象(`invalid` 占比突然异常升高)时,用 > `--rate` 调回更低的值。 > 📌 **新增探针后要跑一次 `funflix db requeue`**。链接落库时若该 provider > 还没有探针,会被写成 `unsupported` 且不排复查时间,之后即使加了探针也 > **永远不会被领取** —— 新链接正常校验、老链接静默地一直停在 unsupported。 探针的第一原则是**判不出来就归 ERROR,绝不归 INVALID**。接口改版、被风控、 返回一段 HTML 错误页,任何一种被判成"链接失效",都会在一轮复查里把整库资源 误杀,而且看起来完全正常(状态是"已确认失效",不是报错)。 `AnonymousHttpProbe` 把这条规则做成了默认行为:`classify` 返回 `None` 即表示 看不懂,骨架翻译成 ERROR —— 新写探针的人**忘了写兜底分支也是安全的**。 ## 设计要点 - **水位用消息 ID 而不是时间**。ID 单调且精确,不受时钟漂移、同秒多条消息、消息编辑改时间戳的影响。 - **水位按「见到的最大 ID」推进**,不是「成功落库的最大 ID」。否则一条无正文的纯图片消息会永久卡住采集。 - **首次接入只取最新一页**。想补历史就显式回拨 `cursor_message_id`,避免接入老频道时把整个历史拉下来。 - **入口按 `content_hash` 去重**。同一条分享被多个源重复抓到时在这里挡住,不会走到后面按次计费的 LLM 抽取。 - **时间统一 UTC-aware**。`UTCDateTime` 抹平了 SQLite(读回 naive)与 PostgreSQL(读回 aware)的行为差异,写入 naive 会直接报错。 ## 开发 开发环境搭建、测试、lint、生成迁移等见 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)。 --- ## 关于 farfarfun [farfarfun](https://github.com/farfarfun) 是一个专注于实用工具库的开源组织, 涵盖云存储、数据处理、AI、多媒体与开发工具链等方向。 - 🏠 组织主页: - 📦 PyPI: - 📧 联系:farfarfun@qq.com 本项目基于 [MIT](LICENSE) 协议开源。