Commit Graph
10 Commits
Author SHA1 Message Date
Noah 67e49354ad fix(recorder): stop stream-gears losing the tail, default to mesio (#1733)
* fix(stream-gears): write the cached GOP on disconnect, flush before the segment hook

The httpflv loop kept the current GOP in `flv_tags_cache` and only wrote it
when the next keyframe arrived. On EOF, a 30 s read timeout or a parse error
the function returned and the cache was dropped, so every disconnect,
reconnect and stream end lost the last 1-10 s. The loop now runs in an inner
function; whatever way it exits, the complete tags in the cache are written
before the original result (including the error) is returned.

`FlvFile` / `TsFile` renamed the `.part` file and ran the segment hook before
the old `BufWriter` was flushed, so the hook (FileValidator, upload) could see
a truncated file, and a failed final flush (disk full) was swallowed. Both now
go through `finish()`: flush and check the error, then rename, then run the
hook. `create_new` returns the flush error; `Drop` logs it with `error!`.

Tests: the rolling-segment test now expects 120 keyframes on disk (was 119)
and checks the size seen by the hook equals the final size; new cases for a
stream that ends cleanly, a read that times out and a parse error; a TS hook
size test; a failed-flush test.

Ref #1726

* feat(recorder): default to mesio when no downloader is configured

Rooms without a `downloader` setting followed the plugin hint, which is
stream-gears for almost every platform. They now use mesio: its FLV output
carries `onMetaData.keyframes` and every segment starts at timestamp 0, so
players can seek; it also records HEVC / Enhanced-FLV and HLS fMP4, which
stream-gears cannot. Only the hint mapping in `core/live.rs` changes; an
explicit `stream-gears` keeps using stream-gears, and plugins that hint
streamlink / yt-dlp are unaffected.

`DownloaderRuntime::from_type` no longer has a catch-all arm. `ffmpeg-external`
and `ffmpeg-internal` used to fall into it and silently run stream-gears; both
now run ffmpeg (external segmenting, same as `ffmpeg`). The arm for
streamlink / yt-dlp / ytarchive, which need the live stream and are built in
`downloader_runtime`, is listed explicitly and logs a warning.

WebUI, README, sample configs and CHANGELOG now say mesio is the default and
list the behaviour differences: the container follows the source (HLS fMP4 is
saved as .mp4), files are written under their final name without `.part`, and
Douyu Wangsu reconnects leave a 2.6-5.8 s overlap between segments.

Ref #1726

* feat(mesio): pass segment duration and size to the segment callback

mesio's segment-complete callback reports the duration and byte size of each
closed file, but biliup only logged them and built `SegmentInfo` with the path
and index. `SegmentInfo` gains `duration_secs` / `size_bytes` (None for the
other downloaders) and mesio fills them in. The end-to-end mesio test checks
the reported size equals the file size on disk.

Ref #1726

* fix(recorder): parse segment_time the same way in every downloader

`segment_time` was read three different ways. stream-gears indexed
`parts[1]` / `parts[2]` directly, so "3600" or "01:00" panicked the download
task (and an unparsable hour silently became 1); mesio returned 0 for anything
that was not HH:MM:SS and silently recorded without time splits; ffmpeg got
the raw string and accepted both forms.

`parse_segment_time` follows ffmpeg's duration syntax (`[HH:]MM:SS[.frac]`
or plain seconds; minutes and seconds below 60 when colons are used), so all
three downloaders now agree. stream-gears and mesio share
`DownloadConfig::segment_time_limit`: 0 means no time split for both (mesio
already did this; stream-gears used to split at every keyframe), and a value
that still cannot be parsed logs a warning and records without time splits
instead of panicking or being ignored silently. The time-range clamp uses the
same parser, so "3600" is shortened near the window end like "01:00:00".

Ref #1726

* feat(webui): enter file_size as a number with an MB/GB unit

The config still stores bytes. The control converts with 1024 (the same
sizes Windows Explorer shows), keeps an untouched value byte-exact even
when it is not a round MB/GB, and re-picks the unit when the form value
is replaced from outside.

In the streamer override, clearing a value that was already set now
saves null (no size split for that streamer); before, Semi dropped the
emptied key and the old value stayed. Clearing something typed in the
same session still falls back to inheriting the global setting.

* fix(webui): keep the file_size unit beside the number in narrow modals
2026-09-24 06:24:17 +00:00
Noah 20fe058b15 fix(ui): 修复 React Compiler lint 命中并恢复规则为 error (#1711)
* fix(ui): 主题与侧栏偏好改用 useSyncExternalStore 读取,去掉 effect 内 setState

react-hooks/set-state-in-effect 命中 4 处,都是「挂载后在 effect 里读浏览器状态再 setState」:

- app/(app)/layout.tsx:117 侧栏折叠偏好、:136 主题模式:两者都是 localStorage 里的值。
  新增 useLocalStorageValue(key),用 useSyncExternalStore 订阅:服务端与水合期快照固定为
  null(与 SSR 输出一致,不会水合不一致),水合完成后 React 自己用真实值重渲染一次;
  写入走 setter 并通知本页订阅者。localStorage 写失败时退回内存值,本页内仍可切换,
  与原先 try/catch 后照常 setState 的行为一致。
- app/lib/utils/index.tsx:66 useSystemTheme:matchMedia 是外部状态,改为 useSyncExternalStore
  订阅 change 事件,服务端快照 light,与原来 useState('light') 再 effect 校正的结果一致。
- app/ui/ThemeButton.tsx:30:原来点击后先置 switchTrigger,再在 effect 里读标志位切主题、
  setMode、清标志位,图标也在 effect 里 setState。改为点击事件里直接算出下一模式并
  applyThemeMode + setMode,图标由 mode 派生,不再有中间 state 与 effect;切换结果相同,
  少一轮渲染。setMode 的类型收窄为 (mode: string) => void,调用方只传字符串。

useTheme 未改:它的首次跳过逻辑仍然成立(首帧主题由 <head> 内联脚本设置)。

* fix(ui): 日志页、空间配置页、账号列表与模板表单去掉 effect 内同步 setState

react-hooks/set-state-in-effect 其余 5 处:

- app/(app)/logviewer/page.tsx:133:connectWebSocket() 开头同步 setIsLoading(true)/setLogs([])
  再建连接,effect 与「刷新」按钮共用。拆开:effect 只负责建立 WebSocket、把消息写进
  state、cleanup 时关闭(上一条连接不再靠 ref 手动 close);清空日志 + 进入加载态移到触发
  重连的事件里(切 Tab 的 onChange、刷新按钮),刷新通过 connectSeq 计数器让 effect 重跑。
  首次挂载时两个 state 本来就是初始值,其余时序与原来一致;顺带删掉不再需要的
  exhaustive-deps 禁用注释。
- app/(app)/dashboard/page.tsx:101:校验失败后先切 Tab / 平台,再在 effect 里滚到出错字段并
  把 pendingField 清空。改为 flushSync 同步提交切面板的 state,随后直接 scrollIntoView +
  focus(React 文档推荐的「更新后读 DOM」写法),去掉 pendingField 这个中间 state。
  onSubmitFail 由 Semi 在 Promise.catch 里调用,不在渲染期,flushSync 合法。
- app/lib/use-streamers.ts:26 useBiliUsers:data 为空时同步 setList([])。改为返回值直接派生
  (无账号 → 空数组常量),effect 里只保留异步补全;data 为空数组时 Promise.all 解析为 []
  仍会把上一批结果清掉,之后再有账号不会先闪旧数据。
- app/ui/TemplateFields.tsx:183:挂载后从 formApi.getValue('dtime') 算滚轮初始选中值。
  Semi Form 在构造函数里就把 initValues 放进 store,首次渲染即可读到,改为 useState 的
  惰性初始值(换算逻辑抽成 delayFromDtime),结果与 effect 版本相同。
- app/(app)/components/BackgroundSetter.tsx:71:hasBg / opacity 挂载后从 localStorage 读。
  改为 useState 惰性初始值;两个值只在面板展开后渲染,SSR 首帧输出不受影响。

* fix(ui): 修复渲染期不纯写法:卡片实时时长、平台插件查找、time_range 转换

- react-hooks/purity  app/ui/StreamerCard.tsx:101:渲染期调 Date.now() 算「已直播多久」。
  新增共享时钟 useNowSec()(app/lib/use-dashboard.ts):模块级 store 由定时器每秒推进,
  组件用 useSyncExternalStore 读取,首个订阅者接入时校准一次。时长拆成 LiveDuration 子组件,
  只有录制中的卡片挂载它,其余卡片不随时钟重渲染。显示值语义不变(开播至今,同一
  formatDuration),刷新节奏从「随 10 s 轮询重渲染时更新」变为每秒更新。timeAgo 内部也用
  Date.now() 但未被规则命中(在工具函数里),本次不动。
- react-hooks/static-components  app/ui/OverrideModal.tsx:297:JSX 里用 IIFE 调 platformSetting()
  拿到组件再渲染,规则把它当成「渲染期创建的组件」。实际是按 url 从模块级常量表
  SupportedPlatforms 取组件。改为先找出匹配的键,再按键从常量表取值,让「同一引用」这一点
  在代码上可见;匹配顺序与结果不变。
- react-hooks/immutability  app/ui/TemplateModal.tsx:121:把 props.entity.time_range 由 JSON
  字串就地改写成 Date[]。改为 useMemo 生成一份本地副本作为 Form 的 initValues,解析失败
  保持原样并 console.error(同原逻辑),不再修改上层传下来的对象。

* chore(lint): 恢复 React Compiler 系列规则为 eslint-plugin-react-hooks 的默认 error

#1710 升级 eslint-plugin-react-hooks 7 时把 set-state-in-effect / static-components /
purity / immutability 四条降为 warn,留给专门 PR 处理。命中的 12 处已在前三个 commit 修完,
这里删掉覆盖,回到插件 recommended 的 error 级别。

* fix(ui): useTheme 不再回写 localStorage,mode 只由 useLocalStorageValue 的 setter 持久化

mode 改由 useSyncExternalStore 从 localStorage 读取后,同一个键有了两个写入方。
StrictMode 下 effect 会在挂载时执行两次,useTheme 的「跳过首次」只能挡住第一次,
第二次会用水合期的默认值 auto 把用户刚保存的主题覆盖掉,再由 store 读回 auto——
`next dev` 里每次刷新主题都会丢(生产构建没有双次 effect,不受影响)。
去掉 useTheme 里的 setItem:切换主题时 setter 已经写过同一个值;系统配色变化时原来会
把 auto 写进去,去掉后键保持缺省,读出来同样按 auto 处理,行为一致。

* fix(ui): 登录页在手机宽度下卡片超出视口、两侧被裁切

review 意见(#1711 下 ForgQi):手机尺寸下登录页两侧被裁切。
原因:三个输入框写死 style={{ width: 440 }},卡片宽度由它们撑成 552px(440 + 56×2 内边距),
.main 居中放置、.frame 又是 overflow: hidden,窄于 552 的屏幕上卡片两侧直接被裁掉。
改法:去掉输入框的固定宽度,卡片定宽 552px 并允许 max-width: 100% 收窄到视口,
.inputs 改为 align-items: stretch 让字段撑满卡片内容区(Semi 的 .semi-input-wrapper 默认 width: 100%),
≤480px 时内边距收到 32px 24px。桌面 1440 浅 / 深色截图与修改前像素 0 差异;390px 下卡片 370px、
输入框 322px、无横向溢出,注册模式(三个输入框)同样。
2026-09-22 08:27:12 +00:00
Noah e988af8268 chore(deps): 升级前端三方依赖至最新稳定版并适配 (#1710)
* chore(deps): 升级 Next.js 14→16、React 18→19 及 TypeScript/ESLint/sass 工具链

- next 14.2 → 16.3.5(Turbopack 成为默认打包器,`output: 'export'` 与 out/ 页面文件名不变)
- react / react-dom 18.3 → 19.3,@types/react(-dom) 同步到 19.3
- qrcode.react 3 → 4、react-json-tree 0.19 → 0.20:仅为满足 React 19 的 peerDependency,API 用法不变
- typescript 5.9 → 6.0.3;tsconfig `target` es5 → ES2017(TS 6 弃用 es5,TS 7 已移除),
  `jsx` preserve → react-jsx(Next 16 强制),include 补 `.next/dev/types`
- next-env.d.ts 改为由 Next 生成、不再入库(官方文档要求,且 dev/build 会写入不同内容;
  仓库 .gitignore 已有 `/*.ts` 规则覆盖)
- eslint 8 → 9.39.5,eslint-config-next 16,eslint-config-prettier 10:
  Next 16 移除了 `next lint`,`.eslintrc.json` 迁移为 flat config `eslint.config.mjs`,
  `npm run lint` 改为直接调用 `eslint .`;eslint-plugin-react-hooks 7 新增的
  React Compiler 规则暂降为 warn(命中的都是既有写法,与本次升级无关)
- sass 1.97 → 1.104、@types/node 20 → 26(仅类型)
- next.config.js 关闭 Next 16.3 新增的 `agentRules`,避免 `next dev` 在仓库根目录生成 AGENTS.md

未升到最新的两项及原因:
- typescript 7.0.2 为原生实现、不再提供 JS API,eslint-config-next 依赖的 typescript-eslint
  明确拒绝 TS 7(`>=4.8.4 <6.1.0`),故停在 6.0.3
- eslint 10 移除了 `context.getFilename()`,eslint-config-next 依赖的 eslint-plugin-react 7.37.5
  尚未适配(peer `^9.7`),故停在 9.x 最后一版

* fix(ui): 适配 React 19 的类型变更

@types/react 19 带来的四类编译错误,逐个按最小改动处理,不涉及运行时逻辑:

- `useRef<T>()` 不再允许省略初始值 → `useRef<FormApi>(undefined)`(6 处,`.current` 类型不变)
- `useRef<HTMLDivElement>(null)` 现返回 `RefObject<HTMLDivElement | null>`,
  logviewer 的 `LogContent` props 类型同步放宽
- 全局 `JSX` 命名空间被移除 → `React.JSX.Element`(AvatarCard)
- `ReactElement` 的 props 默认类型由 `any` 改为 `unknown`,
  TemplateModal / OverrideModal 里 `cloneElement` 注入 onClick 的地方给 `isValidElement`
  标注具体的 props 形状,替代原来的 `<any>`
- 顺带删掉 layout.tsx 里一条已无作用的 `eslint-disable-next-line react-hooks/exhaustive-deps`
  (eslint-plugin-react-hooks 7 对该处不再报告,ESLint 会提示未使用的 disable 指令)

* chore(deps): 升级 Semi Design 2.89→2.103 及 swr/artplayer/mpegts.js,适配 ArrayField.add 新签名

- @douyinfe/semi-ui 2.89.1 → 2.103.0(semi-icons / semi-foundation 随之同步),同一 major,
  用到的 Form / Tabs / Collapse / Modal / Table / Select 等组件 API 未变
- ArrayField 的 `add` 由 `() => void` 变为 `(index?: number) => void`(支持在指定位置插入),
  原来直接写 `onClick={add}` 会把 MouseEvent 当 index 传入且不再通过类型检查,
  7 处改为 `onClick={() => add()}`,行为仍是追加到末尾
- swr 2.3.8 → 2.5.1、artplayer 5.3.0 → 5.4.0、mpegts.js 1.8.0 → 1.8.2:均为 minor/patch,无 API 变更

* chore(deps): 移除无引用的 ncp

`rg ncp` 在 app/、脚本与配置里均无命中(仅 CHANGELOG 提到 #1265 曾把它移出 dependencies),
是早年从 Svelte 前端遗留下来的复制工具,当前构建链(next build → out/ → rust-embed)用不到。

* fix(ui): 站内 Link 关闭预取,消除 Next 16 预取样式触发的 preload 警告

Next 16 的 Link 进入视口即预取目标路由的 RSC 载荷,载荷里的 `HL` 样式提示会让 React 为
目标页的每个 CSS 分块插入 `<link rel="preload" as="style">`;页面停留数秒后 Chrome 会对每个
未使用的预加载报一条 "was preloaded using link preload but not used" 警告。侧栏 9 个链接常驻
视口,配合 Turbopack 的细粒度 CSS 分块,每页会稳定出现 20+ 条(Next 14 下为 0)。

站内导航共 6 处 Link,全部加 `prefetch={false}`:静态导出由本机 axum 直接服务,点击时再拉取
几 KB 的路由载荷,实测 9 个侧栏页面客户端切换均正常且耗时 <150 ms;之后 6 秒停留 0 警告。

本 commit 可独立丢弃:丢弃后功能不受影响,只是控制台会有上述预加载警告。

* fix(ui): 在根布局最先导入 Semi Design 的 React 19 适配器

React 19 从 `react-dom` 上移除了 `render` / `unmountComponentAtNode`,也不再从 `react-dom`
re-export `createRoot`。Semi 的 Toast / Notification / Spin 图标等命令式渲染走
`_utils/reactRender.js`,在 React 19 下既拿不到 legacy render 也拿不到 createRoot,
会打印 "[Semi UI] createRoot is not available" 并**静默不渲染**——表现为保存成功 / 失败的
Toast 与 Notification 全部消失、日志页 Spin 不显示。

按 Semi 官方文档(semi.design/zh-CN/ecosystem/react19)在根布局第一行导入
`@douyinfe/semi-ui/react19-adapter`,它把 `react-dom/client` 的 createRoot 注入
`semiGlobal.config`。验证:空间配置页保存成功 / 校验失败的 Toast 正常弹出,
日志页 console 0 error。

* fix(ui): 消除 React 19 开发模式下的水合属性与 Fragment props 警告

开发模式(StrictMode)逐页走查 13 个页面,React 19 报两类新错误,均为既有写法在新版本下
被更严格地校验,生产构建不受影响:

- 每页一条 "A tree hydrated but some attributes of the server rendered HTML didn't match":
  差异只有 `<html>` / `<body>` 上由 <head> 内联脚本在水合前写入的 theme-mode 属性
  (React 18 下同一原因表现为 "Extra attributes from the server: theme-mode")。
  这是首屏防闪烁的有意行为,按 React 官方做法给这两个元素加 suppressHydrationWarning,
  只屏蔽元素自身的属性差异,不影响子树校验。
- 直播管理页每张卡片一条 "Invalid prop `disabled` supplied to React.Fragment":
  操作按钮以 Fragment 形式作为 Semi ButtonGroup 的唯一子元素,ButtonGroup 会对直接子元素
  cloneElement 注入 disabled / size / type,React 19 起会对 Fragment 上的这些 props 报错。
  改为返回带 key 的元素数组,DOM 结构与样式不变(子元素均非 Button 类型,不会插入分隔线)。

处理后开发模式 13 页 console 仅剩 mock 环境有意返回的 500。
2026-09-22 06:42:07 +00:00
ForgQi 9d02856a50 Integrate mesio (rust-srec) downloader
Add an in-process mesio downloader using the rust-srec mesio-engine. Introduces crates/biliup-cli/src/server/core/downloader/mesio.rs implementing FLV/HLS pull, timestamp fixes, keyframe indexing, and size/duration-based segmentation with pipeline_common, flv_fix and hls_fix. Wire Mesio into downloader enum/runtime and add UI options and public config docs. Update crates/biliup-cli/Cargo.toml and workspace Cargo.toml/Cargo.lock to include new dependencies and bump version to 1.2.5. Include tests for protocol selection, pipeline behavior and an e2e FLV download. Small helper changes: config enum docs and segment handling.
2026-09-21 00:24:52 +08:00
Noah 40234133e3 fix(webui): 保存配置覆写时保留投稿模板关联 (#1685)
覆写弹窗的 entity 白名单漏了 upload_streamers_id,PUT 整行覆盖后外键变 NULL,后续录像不再投稿并走默认 rm。
2026-09-13 15:40:03 +00:00
ForgQi 431d1f61ab Support optional segment limits and danmaku files
Make file_size and segment_time optional and normalize/validate their values so segmentation can be disabled. Frontend: send nulls for empty fields and allow empty segment_time via validation; add showClear to inputs. Downloader: extend SegmentInfo with optional danmaku_file_path and propagate it from various downloaders; DanmakuClient::rolling now returns a bool to signal whether a danmaku file was produced. Uploader: add segment_paths helper to include danmaku files in upload/postprocessing, and preserve processed paths; add unit tests. HookStep: stream child process stdout/stderr as raw bytes to terminal and append to a log file (tee), and simplify run/move logic to use explicit path lists. Cargo.toml: bump reqwest to 0.13.4. Tests and minor adjustments added throughout to support these changes.
2026-06-04 01:19:27 +08:00
ForgQi 92b5a30df5 Add streamer pause/resume feature and UI updates
Introduces a pause/resume button for streamers in the UI and backend, including API endpoint and status handling. Refactors streamer status display, updates related TypeScript interfaces, and improves modal forms. Also updates @douyinfe/semi-ui and related dependencies to v2.86.0, and bumps workspace version to 1.1.10. Minor logging and trait changes in Rust backend for improved clarity and error handling.
2025-09-29 03:18:21 +08:00
bec4f72cc6 feat: 移动端响应式增强 (#1266)
* feat: 弹出层支持响应式宽度

* feat: 优化折叠效果

* feat: 优化移动端折叠体验

* fix: ssr 编译报错

* fix: 修复 PC 切移动端时不会自动折叠的问题

* feat: 支持仅自己可见开关 (#1261)

* feat: 支持仅自己可见开关

* fix: 私密视频投稿失败

* chore: 移除迁移脚本

* fix: 修复 is_only_self 覆盖界面原先额外字段配置问题

* fix: ncp and sass should not production dependencies (#1265)

* feat: 抖音真原画 (#1267)

* feat: 抖音真原画

* add webui option

* Update __init__.py

---------

Co-authored-by: ForgQi <34411314+ForgQi@users.noreply.github.com>

* feat: 优化移动端折叠体验

* chore: rebase master,重新提交 lock

---------

Co-authored-by: XuanRan_Nyan <31968665+xxxxuanran@users.noreply.github.com>
Co-authored-by: ForgQi <34411314+ForgQi@users.noreply.github.com>
2025-04-29 02:03:19 +00:00
Guthrie 518e74f496 feature: 关键词不录播 (#1200) 2025-03-31 02:59:11 +00:00
XuanRan_Nyan 9404dc4a5c feat: Add override configuration support for streamers (#1151)
* feat: Add override configuration support for streamers

* refactor: Simplify plugin imports and platform matching

* refactor: 支持插件配置覆写

* refactor: 更新 User 块覆写

* fix: type error
2025-02-06 12:07:40 +00:00