Centralize authorization into a policy layer so route, field, and user-permission checks all use the same decision point. The frontend now revalidates /v1/me after 403s or dropped connections, and the users page fetches the real role/permission catalog from the backend instead of hardcoding role descriptions. This also tightens field-level protection for sensitive config/template data and avoids exposing user-management controls when auth is off.
The backend and migrations are updated to handle LF/CRLF checkout differences in SQL migration checksums, with compatibility reconciliation for older databases and safeguards to prevent silent drift of previously shipped migrations.
* feat(cli): locate ffmpeg via ffmpeg_path / host / PATH and start child processes without console windows
- New `tools` module in both crates. `tools::command()` starts external
programs with CREATE_NO_WINDOW on Windows; every ffmpeg, streamlink,
yt-dlp, ytarchive and hook shell process now goes through it.
- ffmpeg is looked up in this order: the new optional `ffmpeg_path`
setting, then `ServeOptions::ffmpeg` (for an ffmpeg shipped by the
embedding host), then `ffmpeg` on PATH. Unset keeps today's behaviour.
- `ffmpeg_path` is global (not overridable per streamer) and visible in
the redacted configuration.
* feat(api): GET /v1/tools reports ffmpeg availability, version and license
Read-only, allowed for every logged-in role. Returns whether the ffmpeg
that would be used runs, where it came from (config / bundled / path),
the first line of `ffmpeg -version` (cached per program; failures are
retried) and the license derived from its configure flags. Errors say
what to fix.
* feat(desktop): bundle ffmpeg in the Windows installer, same BtbN build as Docker
- desktop-publish.yml downloads the BtbN win64 GPL static zip of the tag
and version the Dockerfile pins (read by .github/scripts/ffmpeg-version.sh),
checks its SHA-256 and ships only ffmpeg.exe, LICENSE.txt and a README
with the source links as bundle resources (tauri.ffmpeg.conf.json),
installed under ffmpeg\.
- The shell passes resource_dir()/ffmpeg/ffmpeg.exe to ServeOptions::ffmpeg
when the file exists. The ffmpeg_path setting still wins; builds without
the overlay keep using PATH.
* ci(desktop): attach the FFmpeg corresponding source and download notes to releases
- New ffmpeg-source job: FFmpeg at the pinned commit, the BtbN build
scripts at the pinned tag, and the source of every library those
scripts build (fetched by their own download.sh at the commits pinned
in scripts.d/), plus a README and SHA256SUMS. Uploaded on release
events only; manual runs just list the files.
- On release events, append .github/desktop-release-notes.md to the
release body once: setup.exe is the recommended download, and how to
get past the browser warning and SmartScreen while it is unsigned.
* fix(cli): only use CREATE_NO_WINDOW when biliup has no console
`biliup server` in a Windows terminal keeps sharing its console with the
programs it starts, as before: Ctrl+C and their output still reach the
terminal. Only a process without a console window (the desktop app)
starts them with CREATE_NO_WINDOW. The choice is logged at startup and
covered by a Windows-only test that runs itself as a child with and
without a console.
* ci(desktop): link to the upstream FFmpeg source instead of uploading it to releases
Drop the ffmpeg-source job that packed about 2 GB of FFmpeg and dependency
source into every release. The installer's ffmpeg/README.txt and the release
notes now point to the FFmpeg commit and the BtbN/FFmpeg-Builds tree of the
bundled build (full commit hashes, resolved at build time), whose scripts.d/
pins the version and source URL of every library. The Docker image uses the
same BtbN tag, so the same links cover it.
* fix(build): restore editable installs after stream_gears packaging change
Editable installs only put python-source (crates/stream-gears) on sys.path and skip include, so the root biliup/ package was unimportable after maturin develop / pip install -e . Add crates/stream-gears/biliup/__init__.py, which loads the root package under the same name. Wheels are unchanged: maturin only walks stream_gears/ and the include globs, which match the root biliup/ first.
* test(stream-gears): cover editable installs
test_wheel_ships_stubs only checks RECORD for regular installs, since an editable install's RECORD lists the .pth. A new test puts only python-source on PYTHONPATH and checks that biliup and biliup.__main__ resolve to the root package.
* build: move biliup/ and pyproject.toml into crates/stream-gears
Editable installs (`maturin develop`, `pip install -e`) only put
`python-source` on sys.path and skip `include`, so with `biliup/` at the
repository root they could not import it. With `pyproject.toml`,
`biliup/` and `stream_gears/` in the same directory, `biliup` goes in
through `python-packages`, which editable installs honour.
- `biliup/` is moved unchanged; the loader added earlier in this PR is
removed because that path is now the real package.
- `readme` and `license` point at the repository root, so the wheel's
METADATA and `licenses/LICENSE` stay the same.
- CI, Termux and the Dockerfile build with
`-m crates/stream-gears/Cargo.toml`.
Ref #1732
* docs: point source install commands at crates/stream-gears
* feat(cli): let serve run on a host-bound listener with a host-owned shutdown
ServeOptions gains two optional fields for embedding hosts:
- listener: serve on an already-bound socket, so the host knows the
actual port (e.g. after falling back to port 0);
- shutdown: stop when the given future completes instead of installing
Ctrl+C / SIGTERM handlers, which would otherwise take those signals
away from the host process.
The CLI path is unchanged apart from binding the port before opening the
database, so a busy port now fails before data/ is touched.
Ref #1721
* feat(desktop): run biliup server in-process, drop PyInstaller sidecar
The Tauri app now depends on biliup-cli as a library and runs
entry::serve on its own tokio runtime instead of spawning
binaries/biliup.exe:
- listens on 127.0.0.1:19159, or a free port when that one is taken;
the window shows a bundled startup page and switches to the Web UI
once GET / answers 200 (startup failures are shown on that page);
- data/, ds_update.log, cookies.json and recordings live in
app_data_dir(); on first start, data/ left in the install directory
by the old sidecar is copied over and the original is kept;
- closing the window stops the server and waits for it (up to 10 s);
no expect() on exit;
- a second launch focuses the running window instead of starting a
second server on the same data.
Removed: the greet command, the Vite + React scaffold under
tauri-app/src, the shell:allow-execute capability, tauri-plugin-shell,
tauri-plugin-opener, GBK decoding of sidecar stdout and the bundled
binaries/ resource. The Web UI itself is unchanged.
Ref #1721
* ci(desktop): build the Tauri app directly, remove biliup.spec
The Windows desktop workflow no longer builds a wheel, installs it and
runs PyInstaller: it builds the Web UI (embedded into biliup-cli by
rust-embed) and then the Tauri app with tauri-action. Renamed
pyinstaller-publish.yml to desktop-publish.yml.
On release it uploads to the release as before; a manual
workflow_dispatch run only builds and uploads the installers as an
artifact, without touching any release.
The Dockerfile used biliup.spec to tell whether it was built from a
checkout (otherwise it clones master); it now checks pyproject.toml.
Ref #1721
* ci(desktop): smoke-test the installer on manual runs
Only on workflow_dispatch: pretend an old install left data/ in the
install directory, install the NSIS package silently, start the app and
require GET http://127.0.0.1:19159/ to return 200, the database to be
created in the app data directory, the old data/ to be copied there and
kept in place, and the server to stop when the window is closed.
Ref #1721
* feat(desktop): keep data next to the exe unless it is on the system drive
On Windows the data directory is the install directory again, as with the
old sidecar, when the executable is not on %SystemDrive%. When it is, the
first start asks the user (tauri-plugin-dialog): the app data directory
(default), the install directory, or another folder. The choice is saved
as {"data_dir": ...} in desktop.json under the app config directory and
not asked again; a saved directory that cannot be used is reported on the
startup page instead of silently creating a new one. Closing the dialog
quits.
Other platforms have no drive letters and install into read-only
locations, so they keep using the app data directory (or data_dir from
desktop.json) without asking.
When the chosen directory has no data/ yet, data/ left by an older
version is copied there and kept in place. Besides the install directory,
Windows also looks where the old bbup-app installers put the app (their
registry keys and %LOCALAPPDATA%\bbup-app), since a new product name
installs into a new directory.
The data directory is resolved on a separate thread because the dialog
blocks. The migration helpers move to data_dir.rs unchanged.
Ref #1721
* chore(desktop): replace the template names with biliup
- productName bbup-app -> biliup (installers biliup_<version>_x64-setup.exe
and biliup_<version>_x64_en-US.msi, NSIS default install directory
%LOCALAPPDATA%\biliup, Start menu entry biliup)
- identifier com.tauri-app.app -> com.biliup.desktop (app data directory
%APPDATA%\com.biliup.desktop, which is the default data directory
offered on the system drive)
- Cargo package tauri-app -> biliup-desktop (executable
biliup-desktop.exe, so it is not confused with the biliup CLI), lib
tauri_app_lib -> biliup_desktop_lib, drop the template authors
- npm package name, workflow artifact name and the README download link
Tauri derives the NSIS uninstall/registry keys and the MSI upgrade code
from the product name, so the new installers do not replace an installed
bbup-app; both show up in Apps & features. The app copies data/ from the
old bbup-app install directory on first start (see the previous commit),
after which bbup-app can be uninstalled; its uninstaller does not delete
data/.
Ref #1721
* ci(desktop): bump the workflow actions to their latest majors
actions/checkout v4 -> v7, actions/setup-node v4 -> v7,
actions/upload-artifact v4 -> v7 (Node 24 runtimes; the other breaking
changes are about fork checkouts in pull_request_target, caching driven by
a packageManager field and single-file uploads, none of which this
workflow uses). Swatinem/rust-cache@v2 and dtolnay/rust-toolchain@stable
already track the latest release.
tauri-apps/tauri-action v0 -> v1. v1 updates the name and body of an
existing published release it finds by tagName with releaseName and
releaseBody, which are empty here and would wipe the release notes, so
the release job now passes releaseId from the release event instead;
with releaseId the action uploads to that release without looking it up.
Ref #1721
* ci(desktop): smoke-test the new data directory rules on manual runs
Replaces the manual-run smoke step from the earlier smoke-test commit and
can be dropped together with it. On the Windows runner:
- install the v1.2.8 bbup-app NSIS package into C:\old-bbup-app and leave
a data/ there;
- install biliup into its default directory on the system drive (with an
old data/ in it); both products stay installed side by side;
- first start shows the data directory dialog and does not start the
server; UI Automation clicks the default button (the dialog text and a
screenshot are kept as evidence), then GET / must return 200, the
database and the copied data/ must be in %APPDATA%\com.biliup.desktop,
the original kept and the choice saved in desktop.json;
- a second start must not ask again; a preset desktop.json pointing at
another directory stands in for "choose another folder";
- reinstall on D: (or a subst drive when the runner has none): no dialog,
database and log in the install directory, data/ copied from the old
bbup-app directory found through its registry key;
- uninstalling bbup-app keeps its data/.
Ref #1721
* Revert "ci(desktop): smoke-test the new data directory rules on manual runs"
This reverts commit 860c45e89f8e5dab11468c83db7c623b7304e4dd.
* Revert "ci(desktop): smoke-test the installer on manual runs"
This reverts commit b3831f014a2b37f81b71c96e8229a6254bee1b9c.
* feat(stream-gears): ship stream_gears package with accurate stubs
The stream_gears/ package (__init__.py, stub, py.typed) never made it into
the wheel: pyproject only listed python-packages = ["biliup"], so maturin
generated its own __init__.py and the stub drifted from the implementation
(7 of 16 upload lines, login_by_qrcode typed as bool, missing upload
parameters, pyobject.Segment/Credit not accepted by the Rust side).
- Move the package to the repo root next to biliup/ (maturin resolves
python-packages relative to python-source) and set
module-name = "stream_gears.stream_gears" for a mixed layout.
- __init__.py re-exports the native module, adds Segment = PySegment and a
Credit TypedDict; pyobject.py is removed.
- Rewrite stream_gears.pyi from the implementation, including
StreamGearsError and the proxy/file parameters.
- Give upload an explicit text_signature so its string and list defaults
show up in inspect.signature/help() instead of Ellipsis.
Top-level names only grow: Credit and Segment are added.
* ci(desktop): keep PyInstaller off the source stream_gears package
PyInstaller puts the repo root first on its search path, so the source
stream_gears/ (which has no native module) shadowed the installed package
and the frozen biliup failed to import stream_gears.stream_gears. Remove
the source copy after installing the wheel.
* test(stream-gears): check package layout and run stubtest in CI
test_package.py covers the new top-level names, the native submodule,
__all__ staying in sync with the native module, py.typed and the stub in
the wheel, upload's text_signature against PyO3's argument parsing, and
mypy --strict on a valid and an invalid example.
CI runs python -m mypy.stubtest stream_gears against the installed wheel
so the stub cannot drift from the implementation again.
* build(stream-gears): drop unused danmaku-client and async-trait deps
Neither is referenced anywhere in crates/stream-gears. danmaku is still
built through biliup-cli, so this only trims the manifest and lockfile
entries.
* refactor(stream-gears): keep the package in crates/stream-gears via python-source
Point maturin's python-source at crates/stream-gears instead of moving
stream_gears/ to the repository root. python-packages is resolved under
python-source, so the root biliup/ package is shipped through include.
The wheel contents are unchanged.
* ci: drop workarounds for a root-level stream_gears package
With stream_gears/ back in crates/stream-gears, nothing at the repository
root shadows the installed package, so PyInstaller and stubtest no longer
need to avoid it.
* 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
* feat(uploader): public season API handle and BV lookup for archive view
- `SeasonApi` becomes public: `BiliBili::season_api()` returns it, and a
`#[doc(hidden)]` `SeasonApi::with_hosts` points it at other hosts so
callers outside the crate (the CLI tests) can use a local fake server.
The `BiliBili` struct itself is unchanged.
- `archive_view_by_vid(&Vid)` queries the public view endpoint with either
`aid` or `bvid`, so a BV number does not need a separate lookup.
Ref #1721
* feat(cli): biliup season subcommands
`biliup season list | sections <season_id> | add <section_id> --vid … |
remove <episode_id> | sort <section_id> [episode_id…] [--reverse]`, all with
`--json`. `add` fetches cid and title through the public view endpoint.
`sort` puts the listed episodes first and keeps the rest in their current
order, since B站 requires every episode of the section in the request.
The logic lives in `season_cli.rs`; dispatch only gains one match arm in
`main.rs` and in the copy in `stream-gears/src/server.rs`.
Ref #1721
* docs: list season and user subcommands in README and SKILL.md
Ref #1721
* fix(stream-gears): raise exceptions instead of failing silently in the Python API
- Add `stream_gears.StreamGearsError` (subclass of `RuntimeError`, so
existing `except RuntimeError` keeps working). Errors that used to raise
`RuntimeError` now raise it, with the full error-stack cause chain.
- `download` / `download_with_callback`: connection failures, an
incomplete first packet, FLV errors mid-stream and HLS errors raise
`StreamGearsError` instead of panicking (`PanicException` derives from
`BaseException`) or only logging and returning. The FLV path calls
`httpflv::parse_flv`, now public, so `httpflv::download` and server-side
recording are unchanged.
- `download_with_callback` accepts any callable. A callback exception is
logged when it happens and re-raised once the download finishes; if the
download fails too, it becomes the `__cause__` of the `StreamGearsError`.
- `PySegment` fields are readable; `segment` also accepts a dict or an
object with `time` / `size` attributes, and `desc_v2` items accept
objects with `type_id` / `raw_text` / `biz_id` besides dicts.
- `upload`: malformed `extra_fields` raises `ValueError` instead of being
dropped; empty or blank still means none.
- `login_by_sms` raises on failure instead of returning `False`; invalid
`ret` JSON raises `ValueError`; tokio runtime creation errors raise
instead of panicking.
- The seven login functions get `proxy=None` defaults (PyO3 >= 0.24 no
longer defaults trailing `Option` arguments).
Ref #1721
* test(stream-gears): offline pytest cases for error semantics and argument types
Ref #1721
* fix(stream-gears): turn FLV parser panics in download into StreamGearsError
`httpflv::parse_flv` still `expect`s on malformed AAC/AVC/script tag data.
Catch the unwind at the binding boundary so `download` raises
`StreamGearsError` instead of `PanicException`, without changing the
parser that server-side recording shares.
Ref #1721
* test(stream-gears): update smoke suite for StreamGearsError and optional proxy
The login functions now default proxy to None, so the strict xfail cases
for it pass and are removed (test_errors.py covers the same assertion).
StreamGearsError joins the exported names; PySegment fields are readable.
* feat(stream-gears): optional cookie file path for login functions
`login_by_sms`, `login_by_web_cookies` and `login_by_web_qrcode` take a
`file` argument (default `"cookies.json"`, same as before) for where the
login info is written. `login_by_qrcode` takes `file=None` and writes the
login info there when given; it still returns the JSON string.
Ref #1721
* refactor(cli): single entry for native binary and main_loop
Move argument parsing, logging setup and command dispatch from
biliup-cli's main.rs into `biliup_cli::entry`, and make
stream_gears.main_loop call the same entry instead of its own copy.
- Both entries default to `server` when no subcommand is given (or the
legacy `start`), expand `~` in paths, and log to `ds_update.log`
(server only, no rotation) instead of `download.log` on the wheel.
- The log filter honours `RUST_LOG`; an explicit `--rust-log` overrides it.
- main_loop no longer calls process::exit on clap errors: `--help` and
`--version` print and return, other parse errors raise SystemExit with
clap's exit code. Logging uses try_init, so a second call in the same
process does not panic.
- Export `entry::serve(ServeOptions)` so a host can run the Web server on
its own tokio runtime.
- Document config_bindings as deprecated (behaviour unchanged).
Ref #1721
* test(stream-gears): cover the shared entry from the installed wheel
Exit codes of --help/--version/--bad-flag, main_loop called twice in one
process, and biliup server writing ds_update.log with RUST_LOG and
--rust-log precedence.
Ref #1721
* test(stream-gears): update smoke test comment now that --help no longer exits
* build(stream-gears): drop clap, unused after the shared entry
* ci(docker): start the built image and check GET / returns 200
The test job only built the image. Also run it with the default CMD and
require the Web UI to answer and ds_update.log to be written.
Ref #1721
Add BiliBili::seasons, season_section, archive_view, add_to_season,
remove_from_season and sort_season_episodes, matching the endpoints and
payloads of bili_webup.py. Response types keep unknown fields in
`extra` so new fields from Bilibili do not break parsing.
Ref #1721
* build(stream-gears): enable pyo3 extension-module via default crate feature
Plain cargo build/test of stream-gears no longer links libpython, so it
works without Python dev files. build.rs adds the macOS cdylib link args
that maturin would otherwise pass. Wheel builds are unaffected: maturin
already enabled pyo3/extension-module through pyproject.toml.
Ref #1721
* build(stream-gears): drop 13 unused dependencies and biliup-cli python-bridge feature
bytes, ormlite, rust-embed, mime_guess, tower-http, sqlx, serde,
thiserror, futures, async-channel, url, chrono and fancy-regex have no
references under crates/stream-gears/src. The python-bridge feature of
biliup-cli is declared but never used by any cfg or dependent crate.
Ref #1721
* ci(test): run cargo test for the whole workspace including stream-gears
Ref #1721
* test(stream-gears): pytest smoke suite for the wheel in CI
Offline checks against the installed wheel: exported names, UploadLine
members, PySegment/config_bindings basics, `biliup --version` and
`python -m biliup --help` in a subprocess. OPS-19 (login functions
require `proxy`) is recorded as a strict xfail.
The linux x86_64 job installs the abi3 wheel into a venv and runs the
suite before the free-threaded build.
Ref #1721
* chore(stream-gears): move manual scripts to examples/
Drop test_server.py (only printed the default config; now covered by
the smoke suite). Move login.py, test_upload.py and test_download.py to
examples/ so pytest never collects them, and take the video/cookie
paths and stream URL from argv/env instead of hard-coded E:\ paths.
Also pass proxy explicitly (OPS-19), use PySegment (OPS-20) and catch
ValueError from str.index (OPS-38) so the scripts run on this version.
Ref #1721
* test(stream-gears): drop internal bug id from xfail reason
* chore: remove unused setup.py, root .sqlx cache and stream-gears migration copy
setup.py is ignored by the maturin build backend. The workspace has no sqlx
compile-time query macros (only biliup-cli's migrate!()), so the root .sqlx
offline cache is never read, and stream-gears never runs migrations, so its
copy of 1_initial.sql is never executed.
Ref #1721
* chore(stream-gears): remove unconstructed RustDanmakuClient
danmaku.rs defined a RustDanmakuClient that was never constructed nor exposed
to Python; the live one is biliup_cli::server::core::downloader::RustDanmakuClient.
Ref #1721
* chore: drop unused quickjs install from Docker image and Termux wheel
Nothing imports quickjs any more (Douyu signing moved to Rust), yet the Docker
image still compiled it (pulling g++ just for that) and the Termux job injected
it into the wheel's dependencies.
Ref #1721
* build: point Homepage at biliup/biliup and align abi3 floor with requires-python
requires-python is >= 3.9 (yt-dlp and streamlink both require 3.9), but the
extension was built as abi3-py38, producing cp38-abi3 wheels. Bump to abi3-py39
so the wheel tag matches, and make the Termux sed that strips abi3 match any
minor version.
Ref #1721
* build: keep original Homepage URL
Per review, leave the Homepage metadata unchanged.
Ref #1721
* feat(web): multi-user accounts with fixed admin/operator/viewer roles
- migration 6 adds web_users and copies the existing single admin with
the same id and password hash, so upgrades need no action and existing
sessions stay logged in
- table-driven permission middleware on every business route (default
deny), 403 for missing permission, 401 when logged out
- hooks/override and secrets redacted for non-admins; operators may only
pick registered bilibili accounts for templates
- /v1/web-users management API, /v1/me and self password change
- login rate limit: 5 consecutive failures lock the username for 30s
- revoked sessions cut long-lived websocket/stream responses
Refs #1717
* feat(cli): add `biliup user list` and `biliup user reset-password`
Refs #1717
* feat(web-ui): user management page, account menu and editable login name
- /users page (super admin only): create users, change role, reset
password, disable, force logout, delete; table on desktop, cards on
mobile, with loading/empty/error states
- sidebar filters menu entries by the permissions from /v1/me and shows
a no-access state for pages the role cannot open
- account menu in the sidebar footer with change password and logout,
hidden when --auth is off
- 403 responses show a warning toast instead of redirecting
- login page username is editable (default biliup)
Refs #1717
* feat(web-ui): hide actions the current role cannot perform
Streamer add/edit/delete/pause, hook and override editing, template and
upload actions, bilibili account management and config saving are only
rendered for roles that have the matching permission. Account and type
tree requests are skipped for roles that would get 403.
Refs #1717
* fix(web-ui): wait for permissions on restricted pages, surface pause errors
- pages that need a permission not every role has render a spinner until
/v1/me returns, so roles without it never fire requests doomed to 403
- user management is hidden when --auth is off, with an explanation if
the page is opened directly
- the pause button goes through the shared response handler, so a 403
shows the permission toast instead of failing silently
- permission errors sent as plain text keep their message
Refs #1717
* fix(web-ui): keep user actions reachable on tablet and narrow desktop
Refs #1717
* fix(py): handle the user subcommand in the stream-gears entry point
Refs #1717
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.
The Douyu API now returns videoLoop=1 for normal live streams,
causing the video_loop != 0 check to incorrectly filter them out
as looped/replay content. Remove this check and rely solely on
show_status to determine if the room is live.
Co-authored-by: lplalbert <“lplalber111@gmail.com”>
Douyu getH5PlayV1 validates tt against server time; local clock skew
returns error -9 with data:"". PlayResponse could not deserialize the
empty-string data, so the -9/-5 branches never ran. Parse Date from
getEncryption for tt (cache offset for reuse) and deserialize data via
Value so real error/msg surfaces.
Fixes#1680
Co-authored-by: ransxd <ransxd@users.noreply.github.com>
Add optional tid_v2 across Studio, config/DB, CLI, Web UI, stream-gears,
and Python uploaders so new Bilibili partition IDs can be set alongside
legacy tid. Fixes#1662; related to #1562.
Prefix already-resolved progressive HTTP(S) media URLs (e.g. Huya/Douyu
.flv CDN links) with httpstream:// so Streamlink's http plugin matches
them, while leaving HLS/DASH and plugin webpage URLs unchanged.
When a download returns Error (or Err) while the room is still Live,
keep exponential retry backoff instead of resetting retry_count to 0,
which previously caused "Retrying download in 0ns..." spam. Immediate
retry remains only for successful segment completion / clean stream end.
Co-authored-by: ransxd <ransxd@users.noreply.github.com>
This change updates the server default bind to 127.0.0.1 for safer local-only access while documenting how to expose the Web UI via --bind 0.0.0.0 --auth for remote or Docker use. It also removes the previous restriction that blocked first-time admin bootstrap on non-loopback addresses, allowing authenticated remote initialization and bumping the workspace version to 1.2.4.
This change adds a recording policy layer that enforces time-window eligibility and excluded title keywords before probing and before recording. It updates the streamer API to surface reasons like OutOfSchedule and TitleExcluded, clamps ffmpeg/segment durations to the configured window, and exposes the last rejection reason from the worker state for the frontend.
启动 Web 服务时自动登记 CLI 指定的 B 站凭据,并为账号登记、刷新和删除统一补上并发去重、原子写入及引用检查。
同时收紧默认监听、管理员初始化、认证路由、静态文件和日志 WebSocket,恢复 server 的 ds_update.log,并避免凭据及上游响应进入日志。
新增按账号查询远程稿件的 API 与 Web 页面,补齐严格分页和响应校验。
关联 biliup/biliup#1624
关联 biliup/biliup#1658
关联 biliup/biliup#724