176 Commits

Author SHA1 Message Date
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
nianzhibai d424fc0553 feat(spider91): 接入 91porn 爬虫作为新的视频源
把 91VideoSpider/spider_91porn.py 包装成一种 spider91 drive 类型,
每天凌晨自动从 91porn 本月最热第 1 页起翻页,跳过已知 viewkey 凑够
N 个新视频后停止;下载视频和封面到本地,接入现有的视频列表 / 详情
/ 标签 / teaser 流水线。

主要内容:
- Python 脚本:加 --target-new / --seen-viewkeys-file CLI 参数
- 后端:新增 drives/spider91 包(driver + crawler + 测试)
- 后端:catalog.ListVideoFileIDsByDrive 辅助查询
- 后端:crawlerLoop ticker(独立于 02:00-07:00 的网盘扫描循环)
- 后端:HTTP 客户端尊重 HTTPS_PROXY 环境变量 + 每 drive 可选 proxy
- 后端:视频文件后缀按直链 URL 真实后缀决定(mp4/webm/mkv/flv 等)
- 后端:所有 spider91 视频自动打 91porn 标签(source=system)
- API:新增 /p/spider91/{videoID} 路由用 http.ServeFile 服务本地文件
- 管理后台:下拉加 "91 爬虫" 类型;几处特例适配
  (状态显示"已就绪"、操作显示"立即抓取"、扫描根列显示"上次抓取
  N 小时前"、表单隐藏 root_id 等无关字段)
- 文档:README + plan 16 节完整记录

测试:20+ 新增用例覆盖 driver 路径安全、crawler 端到端(伪 python +
httptest 服务器)、扩展名识别、定时窗口判断。
2026-05-22 21:13:26 +08:00
nianzhibai 657b6be981 feat(shorts): TikTok-style shorts mode + long-press 2x playback
VideoPlayer:
- Long-press the video for >=400ms to enter 2x playback rate; release/pause/leave/src change restores 1x
- Block native context menu, iOS long-press callout, and download UI on the player

Shorts page (/shorts) reachable from the main nav burger menu:
- Vertical scroll-snap feed (one video per 100svh slide)
- IntersectionObserver picks the active slide; only active +/- 1 mounts a real <video>
- Default muted autoplay with mute toggle; tap toggles play/pause
- Long-press 2x carries over from the detail player
- Per-slide TikTok-style scrub bar: hidden line by default, drag from the bottom hit area to seek with live MM:SS readout
- Auto-fullscreen on first user pointer (Android Chrome/Firefox/Edge); falls back gracefully on iOS Safari which doesn't support element-level Fullscreen API
- Body overflow lock + dynamic theme-color=#000 while on the page
- Like via double-tap or the heart action button; tap again on the heart to unlike, count synced with the existing detail-page likes
- Heart-burst animation; right-rail action stack ready for future buttons

Backend:
- POST /api/shorts/next: client posts { seenIds, count }, server returns up to N videos that aren't in seenIds, picked via SQLite ORDER BY RANDOM(); roundComplete=true tells client to clear local seen list and start a new round
- DELETE /api/video/:id/like: decrement likes (clamped at 0) for unlike
- catalog.RandomVideosExcluding / CountVisibleVideos / DecrementLike with unit tests

Misc:
- index.html viewport gains viewport-fit=cover for safe-area handling
2026-05-22 09:43:37 +08:00
nianzhibai ce0512d19b refactor(playback): drop transcode/VLC layers, all videos use direct 302
Final decision after evaluating three approaches:
- VLC external player with vlc:// scheme: poor UX, protocol unreliable
- ffprobe + smart remux/transcode: 2-core box gets pinned by ffmpeg
- All-302: simplest and least resource intensive

Removed:
- /p/transcode/{id} routes and full ffmpeg pipeline
- /api/play-token + /p/play VLC bridge
- Server.FFmpegPath/FFprobePath/transcodeJobs fields
- needsBrowserTranscode helper
- VLC button + modal in VideoActions, related CSS
- VideoPlayer transcode polling

videoSource now returns:
- /p/upload/<id> for local uploads
- /p/stream/<driveID>/<fileID> for everything else (302 to CDN)

Trade-off: mkv/avi can no longer be played natively in <video>;
documented in plan section 14.7/14.8 as known limitation.
2026-05-22 00:52:01 +08:00
nianzhibai f3c50c4d29 feat(theme): add dual-theme system with admin appearance page
- Add a global site-wide theme that switches between two palettes:
  - dark + warm orange (existing visual, default)
  - cream white + sakura pink (new, soft cream bg + pink accent +
    deep mauve text + soft pink shadows)
- All colors live in tokens.css under [data-theme="dark"] and
  [data-theme="pink"]; component CSS is unchanged
- Backend: persist theme in SQLite settings table (key=ui.theme),
  expose via Admin Settings PUT/GET and a new public read-only
  endpoint GET /api/settings/theme so the login page can pick up
  the right theme before the user authenticates
- Frontend: inline script in index.html applies cached theme from
  localStorage before React mounts to prevent first-paint flash;
  main.tsx fires syncThemeFromServer() in parallel to align with
  server value; theme.ts validates input and ignores unknown values
  to be robust against an old backend not returning the theme field
- Admin: new /admin/theme page with two large preview cards (mini
  page mock-ups locked to each palette via data-preview), Palette
  icon entry in the sidebar; clicking a card applies locally first,
  then PUTs to the backend with rollback on failure
- README + plan section 14.6 updated
2026-05-21 19:49:28 +08:00