Commit Graph

80 Commits

Author SHA1 Message Date
nianzhibai 66adf444ba fix: detect Docker image version for update checks 2026-05-31 09:55:15 +08:00
nianzhibai 8f8037b838 Update README with service restart instruction
Add note about restarting service on first access.
2026-05-30 20:26:29 +08:00
nianzhibai 215d9596fd Update README.md 2026-05-30 20:17:18 +08:00
nianzhibai e57058db79 feat: prepare v0.0.4 storage release 2026-05-30 20:02:02 +08:00
nianzhibai 6ec61833f2 feat: probe video duration during thumbnail generation 2026-05-30 18:30:22 +08:00
nianzhibai 6e87f88d53 feat: support spider91 uploads to OneDrive 2026-05-30 18:04:15 +08:00
nianzhibai e78fa9d978 feat: improve media generation pipeline status 2026-05-30 17:37:31 +08:00
nianzhibai afbff9eb55 Add Docker Compose deployment support 2026-05-30 11:09:04 +08:00
nianzhibai 039ec2a988 Improve fingerprint dedupe maintenance 2026-05-29 23:58:36 +08:00
nianzhibai da0683344e Add sampled fingerprint deduplication 2026-05-29 23:19:52 +08:00
nianzhibai 1a1282382e Simplify OneDrive setup and redirect playback 2026-05-29 22:35:02 +08:00
nianzhibai 34b6fa8ea9 Release v0.0.3 improvements 2026-05-29 18:34:38 +08:00
nianzhibai 08e38bc4ca Recreate releases with assets 2026-05-29 16:46:02 +08:00
nianzhibai c93d193efe Fetch annotated tag notes for releases 2026-05-29 16:39:12 +08:00
nianzhibai 08568c3951 Use tag notes for release body 2026-05-29 16:34:29 +08:00
nianzhibai 7e394e2971 Prioritize ready thumbnails on home 2026-05-29 16:23:13 +08:00
nianzhibai d16e3168f9 Update README with upgrade instructions and cleanup
Added upgrade instructions for old version users and removed redundant access troubleshooting note.
2026-05-29 15:39:51 +08:00
nianzhibai 81f348b246 Document legacy update recovery 2026-05-29 15:37:40 +08:00
nianzhibai 1e71c1fb72 Wait for service readiness after install 2026-05-29 15:34:48 +08:00
nianzhibai d5122d289e Harden installer update flow 2026-05-29 15:23:42 +08:00
nianzhibai c146ad50ed Fix PikPak captcha recovery 2026-05-29 14:49:47 +08:00
nianzhibai f5c20f9594 Fix spider91 upload target and thumbnails 2026-05-29 06:28:18 +00:00
nianzhibai 62e69d4c06 Update mobile section images in README 2026-05-29 11:54:56 +08:00
nianzhibai 51725ba82f 更新 README.md 2026-05-29 11:28:02 +08:00
nianzhibai c06db836dd Update LinuxDo community link in README 2026-05-28 21:30:38 +08:00
nianzhibai b8717da4fd Include restart command for access issues
Add troubleshooting tip for project access issues.
2026-05-28 21:26:49 +08:00
nianzhibai 2d57545e87 Revise README content for clarity and updates
Updated the README to enhance the description and clarify features.
2026-05-28 21:23:29 +08:00
nianzhibai 6518d772c0 docs: polish README layout 2026-05-28 21:15:40 +08:00
nianzhibai f2c0e7f854 Enhance README with new features and preview images
Added preview images for desktop and mobile, included theme options and short video mode.
2026-05-28 21:11:13 +08:00
nianzhibai 3c7219ecd6 fix: reduce mobile admin content gap 2026-05-28 20:50:46 +08:00
nianzhibai 94669fd35e Revise README for project overview and setup
Updated project description and installation instructions in README.md.
2026-05-28 20:41:40 +08:00
nianzhibai d0159435c0 fix: compact mobile admin navigation 2026-05-28 20:00:38 +08:00
nianzhibai 137cfbcf82 feat: add prebuilt installer workflow 2026-05-28 19:13:41 +08:00
nianzhibai bb8818a55a feat: improve admin setup and drive management 2026-05-28 18:41:40 +08:00
nianzhibai 54ed98f04f style: optimize admin layouts, brand rename to 91 and fix drag-scroll interactions 2026-05-28 17:51:16 +08:00
nianzhibai d2d4db8062 fix: harden spider91 source matching 2026-05-28 16:10:20 +08:00
nianzhibai 7540371838 feat: restore tag classification and drive controls
Restore the previous fixed-tag classification flow, including startup backfill for existing videos and the 91porn spider tag.

Also commit the current drive scanning, preview scheduling, and admin drive-control updates present in the workspace.
2026-05-28 12:18:17 +08:00
nianzhibai e6e5907b38 docs(readme): 用 "视频播放路径" 章节澄清各 drive 走法
之前 README 里 115 段落的 "经过进程内本地代理转发 Range 请求" 句容易被
误读成 "用户播放也走代理",但实际上:

  - 用户播放 /p/stream/<driveID>/<fileID>: 115 / PikPak 走 302 直连 CDN
    (shouldRedirect() in proxy.go 按 kind 判定)
  - ffmpeg 抽 teaser: 才走进程内本地代理(startLocalFFmpegProxy)

新增 ## 视频播放路径 主章节,包含一个三列对比表(用户播放 / ffmpeg 取流)
覆盖所有 kind,并解释三个核心概念:
  - 302 直连意味着什么(带宽、IP 暴露、过期)
  - backend 反代意味着什么(出站流量、Cookie 鉴权)
  - ffmpeg 本地代理为什么需要(避免签名 URL 暴露 + 防多段并发风控)

重写 ### 115 说明,把 "用户播放" 和 "ffmpeg 取 teaser" 两条路径分清楚,
每条以加粗前缀(**用户播放**、**取流优先**、**生成 teaser**、**ffmpeg 访问**)
明确作用域。
2026-05-27 18:48:42 +08:00
nianzhibai 39ef2defcc feat(spider91): 流式爬取 + 完成后统一入队 teaser + 封面失败标 failed
三件相关改动,主题都是 spider91 爬虫流程。

1. 流式爬取协议(取代旧的 "Python 凑齐 15 个再交 Go" 模型)

  Python 端 (spider_91porn.py):
    - 新增 --stream-output flag。开启后每解析出一个 video 直链就把
      entry 作为一行 JSON 写到 stdout 并 flush。
    - log() 在 stream 模式下走 stderr,避免污染 stdout JSONL 协议。
    - --output FILE 仍生效,作离线归档用。

  Go 端 (crawler.go):
    - 新 startSpiderTargetNew() 异步启动 cmd,返回 stdout pipe。
    - RunOnce 用 bufio.Scanner 按行读 stdout,每行解析后立即 processOne
      (下载视频 + 封面 + UpsertVideo)。删掉旧 readSpiderOutput / 全 JSON
      文件解析路径。
    - Python stderr 转发到 backend log,前缀 [spider91:py]。

  收益:Python 翻页找下一个 viewkey 与 Go 下载当前视频在时间上重叠,
  最大化每条签名链接 e= 时间窗。今天观察到 Python 77 秒就找完 15 个
  viewkey 全部 emit;如果还像旧模型那样要等 Go 串行下完才开始下一个,
  后面几个的签名很容易过期(之前 8/15 全 EOF 的根因之一)。

2. teaser 在 crawler 完成后统一入队(取代每条入库立即 enqueue)

  - main.go attachSpider91Crawler 不再注入 OnNewVideo callback。
  - main.go runSpider91Crawl 在 Crawler.RunOnce 完成后调一次
    enqueueDriveGeneration(driveID),让所有新视频统一进 teaser worker。
  - 与 nightly Phase 2 的 "等 teaser 队列 idle" 语义自然对齐。
  - 下载阶段不和 ffmpeg 抢 CPU/IO。

3. 网站封面下载失败时显式标 thumbnail_status='failed'

  spider91 drive 的 thumb worker 按设计不处理 spider91 视频(封面应是
  网站原图直接保存)。当网站封面下载失败时,url='' + status='pending'
  会让 enqueueDriveGeneration 的 waitForThumbnailsBeforePreview 因为
  CountVideosNeedingThumbnail > 0 把 teaser 卡死等待循环。

  修复:crawler.go processOne 中 thumb 失败分支显式标 status='failed'
  (CountVideosNeedingThumbnail 条件 status != 'failed' 会排除)。

  今天观察到的现象:187 MB 视频 c2c04fc8602c5396d469 卡在
  '[preview] waiting for 1 thumbnails before teaser generation'
  循环 35 分钟。

测试:
  - crawler_test.go 重构为 buildFakeSpiderScript helper,
    生成支持 --stream-output 的伪 python(其实是 sh),逐行 echo JSON。
  - TestCrawlerRunOnceFullFlow / TestCrawlerThumbDownloadFailureMarksStatusFailed
    通过新 helper 验证流式协议 + thumb fail 闸门。

go test ./... 全绿;线上手动触发 spider91 抓取验证流式行为正确。
2026-05-27 18:48:30 +08:00
nianzhibai a886b4b490 fix(catalog): thumbnail_status 写入路径同步 + 一次性修历史脏数据
症状:直接 SQL 查到 "thumbnail_status='pending'" 有 4032 行,但 worker 入队
统计、admin API 都显示 0 待生成。

根因:thumbnail_status 是 ALTER TABLE 后加的列(DEFAULT 'pending'),列加入
时所有已有视频的 thumbnail_url 已写好,但 status 全部填了 'pending'。worker
入队按 url 判断(不看 status 字段),所以行为正确,但状态字段长期与 url 不一致。

写入路径修复(避免新写入再产生同类脏数据):
  - UpsertVideo INSERT 列表加入 thumbnail_status,
    值 = CASE WHEN url != '' THEN 'ready' ELSE 'pending' END
  - UpsertVideo ON CONFLICT 按 excluded.thumbnail_url 同步 status
    (url 空时保留原 status,不误改 'failed' 状态)
  - UpdateVideoMeta 当 patch 设了 url 但未传 status 时自动推断 'ready';
    显式传 status 仍然尊重
  - clearVolatileOneDriveThumbnails 清 url 时同步把 status 重置为 'pending',
    让 worker 重新入队

历史数据修复:
  - 新增 reconcileThumbnailStatusOnce(ctx) 一次性 migration
  - 用 marker setting 'videos.thumbnail_status.url_present_to_ready_migrated'
    防重复执行
  - 仅修 url 非空 + status NOT IN ('ready', 'failed') 的行
  - 已在生产 catalog 上跑过,修正 4030 行 (115 drive thumb_pending: 4032→4)

测试覆盖(catalog/tags_test.go +5 个新测试):
  - TestReconcileThumbnailStatusOnce
  - TestUpsertVideoSyncsThumbnailStatusFromURL
  - TestUpsertVideoOnConflictSyncsStatusOnURLChange
  - TestUpdateVideoMetaInfersReadyWhenURLPresent
  - TestClearVolatileOneDriveThumbnailsResetsStatus
2026-05-27 13:18:00 +08:00
nianzhibai 1eeebbf305 refactor(scheduling): 统一三套定时调度为 NightlyJob 流水线
替代 scanLoop / crawlerLoop / Migrator.Run 三个并行的周期循环为单一 nightly.Runner,
每天 cron_hour(默认 01:00)串行跑一条流水线:

  Phase 1  扫所有非 spider91 / 非 localupload 网盘
           → 检测新增视频 + 检测被删视频(清理 catalog 行 + 本地封面/teaser)
           → 入队封面 + teaser(per-drive teaser_enabled 决定 teaser 是否入队)
           → 等所有 thumb / teaser worker 队列 idle
  Phase 2  仅当存在 spider91 drive:跑 91 爬虫,新视频入队 teaser
           → 等 teaser 队列 idle
  Phase 3  spider91 → 云盘迁移(PikPak/115 一次性 sweep)

关键属性:
  - 6h 软超时(nightly.max_duration);到点 phase 跑完,后续 phase 不启动
  - 当天去重:last_run_date 持久化到 settings 表,进程崩溃重启不重复跑
  - sync.Mutex.TryLock 保证手动触发与自然 cron 触发互斥
  - 每 phase 边界检查 ctx.Err,不强 kill 进行中的 ffmpeg / 上传
  - 单 drive '重扫' 和 spider91 '立即抓取' 按钮保留
  - 顶栏新增 '立即跑全流程' 按钮 (POST /admin/api/jobs/nightly/run)

附带优化:
  - preview.Worker / ThumbWorker 增加 WaitIdle(ctx) error,nightly 用作同步屏障
  - scanner 增加 30s 心跳进度日志,避免长扫盘内部黑盒
    格式: [scanner] drive=X progress: scanned=N added=K errors=E dirs=M elapsed=Ts at=<dir>
  - cleanupMissingDriveVideos 从 PikPak-only 扩展到所有云盘 kind
    (保留 stats.Errors==0 闸门避免 API 抖动误删)
  - Migrator 移除周期 ticker / Trigger 通道,改成可单独调用的 RunOnce
    (captcha cooldown 状态机仍保留,跨 RunOnce 持久 5 分钟)

废弃 (字段保留以兼容旧 yaml):
  - scanner.interval_seconds   (替代为 nightly.cron_hour 调度)
  - spider91 drive 的 crawl_hour 凭证字段 (last_crawl_at 仅作 admin UI 显示)

测试:go test ./... 全绿 (含 nightly 包 ~320 行单元测试);npm run build 通过。
2026-05-27 13:17:44 +08:00
nianzhibai ebd6943a10 feat(spider91,drives): 支持上传 115 + 每盘 Teaser 开关
* spider91 → 云盘迁移目标从仅 PikPak 扩展到 PikPak ∪ 115:
  - 115 driver 新增 UploadAndReportSha1(buffer 到 tmp 文件 + sha1 +
    SDK RapidUploadOrByMultipart + 父目录按 sha1 找 fileID)和 Rename
  - migrator 引入 uploadTarget 接口 + pikpakAdapter / p115Adapter,
    按 drive Kind() 路由;catalog 改写 / 本地清理 / 失败冷却 / backfill
    file_name 行为对两种目标盘统一。captcha 冷却仍只对 PikPak 4002/9 生效
  - App.Spider91UploadDriveID 校验放宽到 pikpak ∪ p115,自动选取在两类
    候选并存时拒绝(要求显式选定)
  - admin DrivesPage 在 spider91 表单里加"上传目标"下拉,文案按系统中
    实际挂载的盘 kind 自适应(只挂 PikPak 不会显示 115 字样,反之亦然)

* 全局 teaser 开关下沉为每盘 toggle 按钮:
  - drives 表加 teaser_enabled INTEGER NOT NULL DEFAULT 1
  - 删除 App.PreviewEnabled / SetPreviewEnabled / loadPreviewEnabled
    和 settings.previewEnabled 字段;前端删除 PreviewToggle 组件
  - 新增 catalog.SetDriveTeaserEnabled + POST /admin/api/drives/{id}/teaser-enabled
    接口;AdminServer 加 OnTeaserEnabledChanged hook,从关到开时立刻
    enqueueDriveGeneration 补扫 pending teaser
  - 网盘列表"操作"列加 Power / PowerOff toggle 按钮,乐观更新 + 失败回滚
  - 一次性迁移 resetDriveTeaserEnabledToDefaultOnce:把现存 drive 强制
    重置为开启,marker setting 记号防止重复(兼容短暂存在过的、把全局
    preview.enabled=0 同步成 per-drive=0 的中间版本)
  - 封面 worker 仍始终入队,开关只控制 teaser,避免越权

测试:go test ./... 全绿;npx tsc --noEmit / npm run build 通过。
2026-05-27 12:07:41 +08:00
nianzhibai 95bf67667a fix(spider91): cool down PikPak captcha migration failures 2026-05-27 10:59:12 +08:00
nianzhibai f05df174ac docs(readme): document systemd-based deployment as primary launch mode
- Promote systemd to the primary 运行 method (production / long-running)

- Provide complete unit files for backend (compiled binary) and frontend (vite preview)

- Add daily ops cheatsheet: status / restart / journalctl / rebuild flows

- Keep start.sh as a local-dev / fallback path (方式 B)

- Warn against mixing start.sh with systemd to avoid port/process contention
2026-05-25 18:24:09 +08:00
nianzhibai bd3f27d5b3 fix(pikpak): auto-recover from error_code=4002 captcha_token expired
When PikPak's cached captcha_token expires, Init() and runtime API
calls used to fail permanently with error_code=4002, leaving the drive
un-attached and blocking spider91 -> PikPak migration.

- refreshCaptchaToken: on 4002, clear cached token and retry once with
  empty captcha_token so the server issues a fresh one. Covers the
  driver-attach path during server startup.
- requestOnce: extend captcha-refresh-and-retry path from case 9 to also
  cover case 4002, clearing cache before refresh to avoid sending the
  same expired token again. Covers per-API-call recovery at runtime.
- Add captcha_recovery_test.go covering: recovery on 4002, no-loop
  guard when token already empty, request-level recovery, and
  single-retry boundary.

OpenList's upstream PikPak driver does not currently handle 4002 either,
so this is a strict improvement.
2026-05-25 16:33:41 +08:00
nianzhibai 84ba7c8422 style: redesign web page tab favicon 2026-05-25 13:41:06 +08:00
nianzhibai d920943b58 fix(security): replace reflect-Origin CORS with allowlist (C-1)
Previously corsMiddleware reflected any Origin back into
Access-Control-Allow-Origin while emitting Allow-Credentials: true.
Combined with no CSRF token, this let any third-party site read or
write authenticated APIs cross-origin (full session takeover via
chained requests to /admin/api/drives etc).

Changes:
- config.Server.AllowedOrigins []string (default empty = same-origin only)
- corsMiddleware now only emits CORS headers for whitelisted Origins;
  unknown origins receive no Allow-Origin and 403 on preflight
- '*' entries are silently dropped to prevent regression
- Always set Vary: Origin to keep caches honest
- Drop the originOr() helper, no longer needed
- Add cors_test.go covering allow / reject / preflight / wildcard cases

Same-origin deployments (nginx fronting / and /api on the same domain)
keep working with no config change. Cross-origin deployments must add
their frontend Origin to server.allowed_origins.
2026-05-25 13:28:06 +08:00
nianzhibai e49d5978ee style(shorts): optimize UI and user interaction experience on mobile and desktop 2026-05-23 12:16:07 +08:00
nianzhibai cfeba94d16 feat(scanner): published_at 统一用入库时刻,不再取网盘 mtime
- scanner.go 把 PublishedAt 从 orDefault(e.ModTime, now) 改成 now
- 删除已废弃的 orDefault 工具函数
- README 把'发布时间'语义点透为'即视频入库时刻'
- plan 追加 14.2.10 记录这次决策

历史数据不回填;新扫的视频起按新规则。
2026-05-23 11:29:47 +08:00
nianzhibai ada69fec87 feat(pikpak): 302 重定向播放 + 自动迁移 spider91 视频
- PikPak 视频播放从反代切到 302 直连 PikPak CDN(与 OpenList 一致),
  浏览器直接拿签名链接,backend 不再消耗带宽转发字节。
  proxy.shouldRedirect 改成 switch,pikpak 与 p115 同等处理。

- 实现 PikPak Driver.Upload:参考 OpenList 协议,先算 GCID
  (SHA1-of-SHA1-blocks 自定义 hash,OpenList 同款)申请上传会话;
  命中秒传直接返回 file id,否则用 vendored 的 aliyun-oss-go-sdk
  PutObject 走 S3 兼容上传。单次 PutObject 上限 5GiB-1。
  另加 PikPak.Rename(PATCH /drive/v1/files/<id>)。

- 新建 internal/spider91migrate 包:周期把 spider91 爬的视频上传到
  指定的 PikPak drive,事务性改写 catalog 行(drive_id / file_id /
  file_name / content_hash),删本地 mp4+thumb。视频 ID 保持
  spider91-<driveID>-<viewkey> 不变,video_tags / views / likes /
  91porn 标签全部保留。catalog 加 MigrateVideoToDrive +
  ListVideosByDriveID + ListSpider91Viewkeys。

- 上传策略:本地保留最新 KeepLatestN=15 个文件,超出部分(更旧的)
  才上传到 PikPak。第一次爬完 15 个全留本地不上传;第二次爬完 30 个
  时把最旧 15 个迁走。稳态本地 ≤15 个最新视频,PikPak 累积所有历史。

- 文件名方案 B:上传到 PikPak 时用 <sanitized title>-<viewkey后8>.<ext>,
  catalog file_name 同步更新;启动时 backfillFileNames 幂等地把已迁
  视频的旧名(viewkey.ext)改成新格式。

- crawler 完成后立即 ping migrator,不必等 60s 周期。

- 修一个迁移破坏去重的 bug:crawler 写 seen viewkey 时按 drive_id 查,
  但视频迁到 PikPak 后 drive_id 不再是 spider91。改用 ListSpider91Viewkeys
  按 id 前缀 'spider91-<driveID>-' 查,迁移后仍能识别。

- 加全局设置 spider91_upload_drive_id(settings 表)+ admin GET/PUT API;
  未显式设置时自动选取唯一的 PikPak drive。

- 顺手清理已废弃的 RemoteDir / preview 回写网盘相关代码(teaser+封面
  早就只走本地,但残留了 Config 字段、yaml 示例、NewWorker 多余参数、
  catalog UpdatePreview 多余参数)。

测试:
- 新增 ~40 个单测覆盖 GCID 算法、PikPak Upload/Rename schema、
  migrator 各种场景(保留窗口内/外、上传失败、未配 target、批次限流、
  孤儿清理、文件名 backfill 幂等)、文件名 sanitize、PikPak 302 重定向。
- 全包 go test -count=1 通过。

联调:在生产实例上验证:spider91 17 条已迁视频 6 秒内全部秒传到 PikPak、
catalog 改写正确、本地清空、PikPak 视频回放走 302 直连
dl-z01a-0043.mypikpak.net;触发新爬 15 条本地保留不上传;
backfill 把旧 viewkey.mp4 命名改成 <title>-<viewkey后8>.mp4。
2026-05-23 02:01:36 +08:00