Commit Graph

1896 Commits

Author SHA1 Message Date
Estrella Pan
c8f402fd68 chore: bump version to 3.3.0-beta.2
CHANGELOG: fold the unreleased in-app updater + multi-provider LLM
entries into 3.3.0-beta.2 and add the WebUI UX/a11y/perf pass and
post-review backend fixes. docs/changelog/3.3.md (the release body)
gains the beta.2 highlights.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
3.3.0-beta.2
2026-07-03 16:23:38 +02:00
Estrella Pan
39564a8a54 polish(webui): UX/a11y/perf pass from live review + parallel audits
Destructive-action safety:
- Rule-delete dialog no longer deletes on "No": clear message, "also
  delete downloaded files" checkbox, real Cancel + single red Delete
- RSS bulk delete, torrent bulk delete, clear-log and notification
  provider removal now behind NPopconfirm

Honest feedback and data states:
- axios interceptor gains per-request `silent` flag; background fetches
  (status/log/downloader polls, passkey list) stop toasting errors
- failed downloader poll keeps last data instead of faking "no torrents";
  failed first bangumi load shows error+retry instead of onboarding;
  search SSE failure distinct from "no results"; RSS table loading state;
  calendar refresh + provider save success toasts; wizard submit failures
  show the backend reason

Setup wizard dead ends:
- downloader step no longer hard-gated on a passing connection test
  (passwordless qB testable, editing fields re-arms untested state)
- inline username validation; progress persists to sessionStorage

Accessibility and touch:
- keyboard: logout button, search variant/filter chips as real buttons,
  Space on card buttons, Escape closes add/edit modals, rule-list rows
  focusable; visible close button on the search modal (was backdrop-only)
- air-weekday select in the rule editor — weekday assignment now works on
  mobile/keyboard, not just desktop drag
- mobile nav actually fixed to the bottom (was rendering in-flow at top)
- 44px touch targets: topbar, chips, modal close, offsets, fold headers,
  wizard controls (with 16px inputs to stop iOS zoom)

Performance:
- self-hosted Inter variable font replaces render-blocking Google Fonts
- lazy poster loading on the landing grid; log page bounded to newest
  1000 lines; KeepAlive capped at 3 pages

Design tokens: new warning bg/border/text tokens (light+dark) replace
hard-coded ambers; add-RSS and notification config use NSwitch/NSelect
instead of hand-rolled controls; ab-data-list selection is controllable
(mobile RSS checkboxes no longer desync after bulk actions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 16:18:27 +02:00
Estrella Pan
d28f5d5e1d fix(qb): rename verify must see new_path before reporting success (#754 #749)
The verify loop treated "neither old nor new name in the file list" as a
successful rename. A qBittorrent 200 that never materializes (target name
taken by a duplicate from another source, file list anomaly) was reported
as success, so the renamer re-renamed and re-notified every rename cycle.
Now only an observed new_path returns True; everything else returns False
and backs off via _PENDING_RENAME_COOLDOWN.

Regression tests adapted from PR #1037 by cyber-ayi, which diagnosed the
false-positive contract.

Co-authored-by: cyber-ayi <cyber-ayi@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 14:39:21 +02:00
Estrella Pan
d7e3f38732 fix(types): unbreak mypy on 3.3-dev after ed25519 signing commit
- boot_overlay/signing: narrow load_pem_public_key result to
  Ed25519PublicKey before verify; a non-ed25519 PEM is now rejected
  explicitly instead of via TypeError caught by the broad handler
- test_context: drive the scheduler mock through a local variable so
  mypy does not see an assignment to the read-only Scheduler.running
- test_migrations_module: _make_versioned_engine returns Engine

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 14:31:39 +02:00
Estrella Pan
27c04f5f5d feat(update): sign update bundles (ed25519) and stop root uv sync (P1)
Closes the update-chain trust gap: the sha256 came from the same GitHub
release as the bundle (integrity, not authenticity), and the boot overlay
re-applied ab-writable content as root — including uv sync on an
attacker-controllable lockfile.

- signing: new module/update/signing.py verifies an ed25519 signature;
  public key embedded in the image at /app/ab_update_pubkey.pem (outside
  the overlay-replaceable tree, so it is the trust root). Private key lives
  only in CI (UPDATE_SIGNING_KEY secret).
- updater: discover the .zip.sig asset, verify the signature after the
  sha256 check, reject any release without a signature, retain the verified
  bundle.zip+.sig for boot re-verification, and swap them on rollback.
- boot_overlay: re-verify the signed bundle at every boot and extract the
  module tree / webui dist FROM the verified zip — never from the
  ab-writable current/ tree; read the version from the zip manifest, not
  the ab-writable applied.json; drop to the ab user before any fallback
  uv sync so lockfile build hooks can't run as root; zip-slip guard uses
  is_relative_to (also fixed in updater._safe_extract).
- CI: build.yml signs the bundle with UPDATE_SIGNING_KEY and publishes the
  .zip.sig; the build fails if the secret is unset.
- deps: cryptography made an explicit dependency.

Tests cover bad-signature reject, unsigned-release reject, legacy overlay
without a signed bundle reject, verified-bundle retention, and boot-overlay
signature verification. Full backend suite: 1079 passed.

Ops: run `gh secret set UPDATE_SIGNING_KEY < ~/.autobangumi/update-signing-key.pem`
before the 3.3 release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 11:15:03 +02:00
Estrella Pan
98e016ce2e fix: apply architecture-review fixes P2-P10 across update, downloader, core, parser, webui
- update: snapshot DB before promote; rollback restores it only when the
  schema actually advanced, never from a stale on-disk fallback; snapshots
  are standalone (no WAL sidecars); dependency sync moved to apply time
  with a staged venv, plus boot-time venv reconcile after rollback;
  entrypoint restart loop forwards signals so docker stop stays graceful
- downloader: AddResult carried through the client protocol; qB "Fails."
  only classified duplicate when the magnet hash is confirmed present;
  aria2 partial-success batches report ADDED, magnet followedBy gids are
  migrated and metadata stubs skipped in torrents_info; renamed paths
  persisted per gid so the rename loop converges
- core: AppContext lifecycle serialized under one lock with running state
  derived from the scheduler; setup wizard starts background loops on
  completion
- parser: LLM path gains episode_type classification (LLM results only),
  timeout/cache/concurrency/breaker controls, proxy-aware SDK clients
  closed after a grace period on config reload
- webui: useApi returns an {ok} discriminant; config save and RSS refresh
  surface failures instead of false success
- migrations: per-statement guards so a crash mid-migration recovers

Implemented via Codex (P2-P10 from tasks/todo.md), then reviewed with a
multi-agent verify pass; 10 confirmed review findings fixed on top.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 10:52:09 +02:00
Estrella Pan
b34fa62403 feat(update): in-app auto-update via volume-backed overlay
Adds one-click update from the settings page so routine updates no
longer require pulling a new Docker image.

- CI attaches update-bundle-<version>.zip (+ .sha256) to each release:
  backend module tree, built webui, and a manifest; the image bakes
  /app/IMAGE_VERSION as the baseline
- boot_overlay.py (shipped in the image, outside /app/module so it can
  never overwrite its own deciding logic) runs from entrypoint before
  the app starts: if a persisted overlay in /app/config/updates is
  newer than the image it tree-replaces /app/module and /app/dist and
  runs `uv sync` when the lockfile changed; if the image is newer it
  clears the stale overlay. Applied via the config volume, so updates
  survive container recreate; a newer pulled image always wins
- GET/POST /api/v1/update/{check,apply,rollback} (auth-gated, repo
  pinned to EstrellaXD/Auto_Bangumi): check queries GitHub releases
  with a stable/beta channel; apply downloads, verifies sha256, stages,
  promotes current->backup, then exits the process so Docker restarts
  into the overlay; rollback swaps the backup back. Progress streams
  over the existing SSE channel
- Settings card (Log page) shows current/latest, release notes, channel
  toggle, update + rollback, with a restart:unless-stopped requirement
  note; compose examples updated

Requires restart: unless-stopped for self-restart (documented; no
Docker-socket fallback). 1043 backend tests, mypy 0, ruff clean; 135
frontend tests, vue-tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-03 09:22:34 +02:00
Estrella Pan
93d3f1a947 feat(parser): multi-provider LLM parser with selectable fallback/primary mode
Generalizes the OpenAI-only title parser into a provider-dispatching
LLMParser (openai-compatible / Anthropic / Gemini) with a parse-mode
switch:

- fallback (default): regex parser runs first, LLM only rescues titles
  the regex can't handle — minimal LLM spend
- primary: LLM parses every title first, regex is the safety net so no
  title is dropped on LLM failure

The openai provider takes a base_url, covering any OpenAI-compatible
endpoint (DeepSeek, Ollama, LM Studio, OpenRouter, OneAPI). Anthropic
uses structured JSON output with refusal handling; Gemini uses
response_mime_type=application/json.

Config auto-migrates the legacy experimental_openai section into a new
llm section on load (migrated users get mode=primary, preserving their
original LLM-first behavior); the old section is kept for downgrade
safety and llm.api_key is redacted by the config API. Settings page
swaps the OpenAI panel for an LLM panel with provider + mode selects.

1018 backend tests, mypy 0, ruff clean; 131 frontend tests, vue-tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-03 08:51:01 +02:00
Estrella Pan
e67dff8f6b chore: bump version to 3.3.0-beta.1, full release notes for the 3.3 beta
docs/changelog/3.3.md rewritten to cover the complete 3.3 scope (it is
the release body the tag build publishes): architecture, aria2
first-class, movies/specials, preference dedup, notifications, security
stack, PyJWT migration, performance pass, WebUI/SSE work, upgrade notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
3.3.0-beta.1
2026-07-03 08:10:47 +02:00
Estrella Pan
a6d0ac8718 perf: precompiled parser regex, batched renamer fallback, restored lost optimizations
Audit of the Jan perf pass (cba4988) against the 3.3 rewrite found two
regressions and applied new profile-guided fixes:

- raw_parser: all static regex precompiled — 16% faster wall, re._compile
  calls down from 2.06M to 89k per 100k parses
- Renamer untagged-fallback offset lookup batched (same idiom as
  engine/auto-tag): 215ms/201 queries -> 19ms/2 queries at 200 torrents
- Notification dispatch across items re-parallelized (lost in the
  rewrite), after fixing the underlying RequestURL re-entrancy race
  that made naive gather unsafe
- Migrations 13/14: index backfill for upgrading databases — index=True
  only materializes via create_all on fresh DBs, so every upgraded
  install was full-scanning title_raw/url/rss_id lookups; new
  ix_torrent_bangumi_id flips offset-scanner and preference-dedup
  queries from SCAN to SEARCH

990 tests green (+6 regression tests), mypy 0 errors, ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-03 08:03:09 +02:00
Estrella Pan
e5e8c91699 chore(webui): clear baseline lint errors now gating CI
The dev-branch CI added in this cycle runs pnpm lint, so the
pre-existing eslint errors (import sorting, unused imports, a
message-less Error in a test) started failing the pipeline. Auto-fixed
plus one manual message; remaining items are warnings only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-03 07:11:20 +02:00
Estrella Pan
edbc734d08 fix(security,sse): restore 72-byte password semantics, bound SSE downloader fetch
- bcrypt 5.x raises on >72-byte passwords where passlib silently
  truncated; hash and verify now truncate to 72 bytes so pre-upgrade
  users with long (e.g. CJK) passwords can still log in instead of
  hitting HTTP 500
- The SSE downloader payload is fetched under a 3s asyncio.wait_for
  (task cancelled cleanly on timeout) and emits an explicit null on
  failure, so a dead qBittorrent can no longer freeze the entire
  event stream while the UI shows connected

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 22:07:12 +02:00
Estrella Pan
d422e615b1 fix(renamer): distinct names for multi-file movies, revert group-tag prefix, season floor
- Multi-file movie torrents: the largest file keeps "Title (Year).ext",
  extras get "Title (Year) - <original-stem>.ext" (idempotent across
  rename cycles) — no more per-cycle qB conflicts or aria2 collisions
- Reverted the "[group] " filename prefix: group_tag historically only
  named the qB RSS rule; prefixing renames would mass-rename existing
  seeding libraries on upgrade
- Season floor restored for regular episodes (season+offset<1 reverts
  to the original season); only specials may land in Season 0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 22:07:12 +02:00
Estrella Pan
1f5a6195d8 fix(aria2): reconcile stale dedup rows, guard zero-length progress, never overwrite on rename
- On a dedup-key hit the gid is verified via tellStatus; rows for
  downloads removed outside AutoBangumi are deleted so the torrent can
  be re-added (unreachable aria2 conservatively keeps the row)
- JSON-RPC numeric strings are parsed before math; totalLength "0" no
  longer raises ZeroDivisionError in torrents_info
- Filesystem renames refuse to overwrite an existing destination and
  fail loudly instead of destroying data via os.replace

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 22:07:12 +02:00
Estrella Pan
eada4e7210 fix(rss): retry failed adds, stop spurious failure alerts, unpollute offset scan
- Failed torrent adds are no longer persisted, so the feed item
  re-enters the pipeline on the next tick — the previous
  downloaded=False row blocked retry forever via check_new's URL dedup
- add_torrent returns AddResult (ADDED/DUPLICATE/FAILED); healthy
  duplicates no longer emit download-failure notifications
- bangumi_id is set only on torrents that pass the exclusion filter,
  so OffsetScanner no longer computes suggestions from excluded
  episodes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 22:07:12 +02:00
Estrella Pan
8597e3d64f polish(webui): declutter RSS table, log page copy, nav labels, title clamps
- RSS: parser/aggregate chips move next to the feed name where they
  describe the source; Status keeps only Connected/Active state; URL
  column left-aligned with correct "URL" header; chip labels localized
- Log: "Contact Infomation" typo, "Update Now"→"Refresh" and
  "Reset"→"Clear log" (the buttons refresh/clear the log view, not the
  app), empty state message instead of a bare box
- Sidebar/mobile nav: "HomePage"→"Home", "RSS Manager"→"RSS" — labels
  no longer truncate in the mobile bottom nav
- Bangumi cards: titles clamp to two lines instead of one and carry a
  full-title tooltip
- Calendar: drop the redundant "Schedule" heading under the page h1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 21:46:09 +02:00
Estrella Pan
ddd618b733 polish(webui): calendar card titles, table/header fixes, wizard and contrast
- Calendar cards without a poster now show the series title on the
  placeholder (was an anonymous gray box until hover)
- Downloader: Seeds/Peers column no longer wraps mid-word; progress
  percentage moved outside the bar where it is readable at 0%
- RSS: Refresh All icon uses NButton's icon slot (was glued to label)
- Setup wizard: progress bar counts the current step (was empty on
  step 1), animates via transform with reduced-motion fallback,
  description text raised to AA contrast
- Card titles use the primary text color — headers were lighter than
  body text (inverted hierarchy) across every card

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 21:33:02 +02:00
Estrella Pan
d80bf89cfa fix(webui): restore config page — naive-ui theme crash and missing controls
Two regressions from the component-system unification left the Config
page blank or control-less:

- App.vue passed var(--color-danger) as common.errorColor*; naive-ui
  runs JS color math (seemly rgba) on these tokens and threw
  "Invalid color value var(...)" while rendering NSelect, killing the
  whole page subtree. Error colors are now per-theme literals.
- ab-setting.vue still referenced the deleted AbSwitch/AbSelect
  primitives, which auto-resolution silently rendered as nothing —
  every switch/select in settings vanished. Replaced with themed
  NSwitch/NSelect plus an items→options adapter for the old API.
- Bumped naive-ui 2.39→2.44: 2.39 predates Vue 3.5 and crashed NTag
  rendering inside NDynamicTags ("theme.self undefined").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 21:33:02 +02:00
Estrella Pan
a630d10cde feat(dev): one-command dev environment via scripts/dev.sh
up/down/seed/status/reset/shot subcommands: reuses the e2e docker
services (qBittorrent + mock RSS), starts backend with AB_DEV_NO_AUTH=1
and the webui dev server, pins a stable qB password, completes the
setup wizard through the API, and subscribes the mock feed — a seeded,
login-free dev stack in one command. shot provides a headless-Chrome
screenshot fallback (the bundled Chrome-for-Testing hangs on
Page.captureScreenshot on macOS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 21:33:02 +02:00
Estrella Pan
ddc3717121 test(e2e): update stale expectations for POST conversions and 404 semantics
The e2e suite never runs in CI and drifted from three intentional changes:
- destructive endpoints (start/stop/restart, log/clear, logout, bangumi
  reset) were converted from GET to POST for CSRF hardening
- disabling a nonexistent RSS now returns 404 instead of 406
- newer qBittorrent images return 204 with no body on login

Full e2e run now green: 67 passed, 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 20:02:35 +02:00
Estrella Pan
76c4f72a95 docs(changelog): summarize big-rocks phase (aria2, movies/specials, release preference, webui refactor)
Add Unreleased entries covering the aria2 first-class downloader, movie/
special episode recognition, per-bangumi release preference dedup, and
the webui component-split/naive-ui/SSE work landed this phase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:49:51 +02:00
Estrella Pan
3baf1c72b3 refactor(webui): split oversized components, adopt naive-ui primitives, add SSE
Extract useBangumiRuleForm + shared preview/tag/rss-link/filter/offset
components out of ab-add-rss.vue and ab-edit-rule.vue, and split
calendar.vue into calendar-board/calendar-day-column/calendar-card/
calendar-mobile-list/calendar-rule-list-popup. Replace the bespoke
ab-button/ab-select/ab-switch/ab-checkbox primitives with themed
NButton/NSelect/NSwitch (naive-ui) across the app and delete the now-
unused wrappers. Add a backend SSE endpoint (GET /api/v1/events/stream)
streaming status/downloader/log ticks, and a shared useEventStream
composable that the status/downloader/log polling loops prefer over
their interval fetch, falling back to polling when SSE is unavailable
or the tab is hidden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:48:46 +02:00
Estrella Pan
590220abfe feat(parser): recognize movies and specials as non-episodic bangumi
Teach raw_parser to detect 剧场版/劇場版/电影版/Movie tokens (episode_type
"movie") and OVA/OAD/SP/Special tokens (episode_type "special", season
forced to 0) instead of returning None. Add episode_type to Episode/
Bangumi/BangumiUpdate/EpisodeFile/SubtitleFile (migration v12), fall
back to TMDB's search/movie endpoint when search/tv finds no animation
match, and rename/organize movies with a flat "Title (Year)" layout
(no Season subfolder) while specials use Season 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:48:31 +02:00
Estrella Pan
27049dedc5 feat(downloader): make aria2 a first-class downloader backend
Replace the aria2 client's stub RPC layer with a typed, per-call-timeout
JSON-RPC implementation and real add/query/rename/move/delete support:
honest add-result tracking with local dedup (url/file-hash) plus
RPC duplicate-error detection, torrents_info/torrents_files mapped into
the qB-shaped dict the rest of the app expects, and a new aria2_gid
table (migration v11) persisting the "ab:<bangumi_id>" tag association
so Renamer's tag-based offset lookup works unmodified. Capability flags
(can_query/can_rename/can_manage) now reflect what is actually
implemented; can_rss_rules stays False (no qB-native RSS-rule surface
to honestly back it with).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:48:16 +02:00
Estrella Pan
cbab57cd05 feat(rss): add per-bangumi release group/resolution preference with dedup skip
Add preferred_group/preferred_resolution fields on Bangumi (migration v10)
and wire RSSEngine.refresh_rss to skip strictly-worse candidate releases
within a batch, or against an already-downloaded episode, when a
preference is set. Bangumi without a preference are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:48:06 +02:00
Estrella Pan
66dea1af0f chore(types): add mypy to CI and fix ~20 bugs it surfaced
Add mypy>=2.1.0 as a dev dependency with a check_untyped_defs baseline
config and per-module ignore overrides (module.__version__, httpx_socks,
aiohttp, test.* method-assign), and run it in CI right after ruff.

Convert positional Field(value, ...) defaults to Field(default=value, ...)
across models/config.py so pydantic v2 stops treating aliased fields as
required constructor args. Fix real bugs the sweep surfaced: unguarded
None bangumi_id in TorrentManager.refind_poster, RSSAnalyser.torrent_to_data
silently returning None past its declared type, missing __aenter__ guards
on RequestURL/downloader clients, database/engine.py's sync sessionmaker
on an AsyncEngine, and RequestContent.post_json shadowing RequestURL.post_json
(renamed to post_form_json). Remove a dead, type-unsafe str_to_list() helper
in api/bangumi.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:00:37 +02:00
Estrella Pan
72c4a040e4 feat(parser): per-provider parser choice, OpenAI as raw_parser fallback, honor group_tag
Store {url, parser} per search provider (backward-compatible with
legacy url-only configs) and use it in search_url() instead of the
hardcoded mikan-vs-tmdb rule.

TitleParser.raw_parser() now always tries the regex parser first and
only falls back to OpenAI when regex parsing fails and
experimental_openai.enable is set.

Renamer.gen_path() prefixes filenames with "[Group] " when
bangumi_manage.group_tag is enabled and a group tag was parsed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:00:18 +02:00
Estrella Pan
ca58fdc50e feat(offset): compute offset suggestions from real parsed episodes and expose apply endpoints
Replace the offset scanner's hardcoded parsed_episode=1 placeholder with
_get_latest_parsed_episode(), which parses a bangumi's actual torrent
names via raw_parser() and skips the suggestion when nothing parses.

Add BangumiDatabase.apply_offset() to copy suggested offsets into the
live season/episode offsets and clear the review flag, plus
POST /api/v1/bangumi/apply-offset/{bangumi_id} and .../apply-offset/many,
each triggering a rename pass afterward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:00:11 +02:00
Estrella Pan
bf84d91926 feat(notification): typed system events, per-provider templates, composition-based base class
Introduce SystemEvent (RssFailureEvent/DownloadFailureEvent/OffsetReviewEvent)
raised by refresh_rss() on healthy->error transitions and add_torrent()
failures, and by the offset scanner; wire them through NotificationManager.send_event()
and the rss/offset-scan loops (gated by settings.notification.enable).

Extend the {{title}}/{{season}}/{{episode}}/{{poster_url}} template mechanism
from webhook-only to all providers via NotificationProvider._format_message(),
falling back to each provider's original default message when unset.

Replace NotificationProvider's multiple inheritance from RequestContent with
composition (self._http), removing the post_json self-shadowing workaround.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:00:00 +02:00
Estrella Pan
820ff29537 refactor(security): migrate JWT/password hashing from jose+passlib to pyjwt+bcrypt
Drop python-jose and passlib in favor of direct pyjwt (with pinned
decode algorithm) and bcrypt 4.x hashpw/checkpw calls. Existing
passlib-generated $2b$ hashes keep validating unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 18:58:22 +02:00
Estrella Pan
92eb224011 feat(webui): add RSS refresh buttons, fix vitest API contract tests going stale
Add per-row and 'refresh all' buttons to the RSS page, wired through
new store actions refreshRSS/refreshAllRSS backed by apiRSS.refresh(All).
Fix vitest.config.ts's auto-import dirs being out of sync with
vite.config.ts, which silently made every src/api/*.ts contract test a
no-op (bare axios/apiXxx identifiers threw under vitest, so the specs
never called real code). Add a shared axios mock helper and give every
src/api/*.ts module a real contract test asserting path + method against
the mocked axios instance and cross-checked against the FastAPI routers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 17:39:59 +02:00
Estrella Pan
c2261ece0a docs(ci): make release changelog lookup version-aware, add 3.3 changelog, fix docker-compose PGID/PUID
Derive the release changelog path from the release version instead of
hardcoding 3.2.md, so the workflow keeps working after 3.3 ships. Add
docs/changelog/3.3.md summarizing the 3.3.0 section of CHANGELOG.md.
Fix docker-compose.yml's PGID/PUID falling back to shell substitution
that never runs in a compose file, using numeric defaults instead, and
drop the obsolete top-level 'version' key. Fix README's Python badge to
3.13 to match backend/pyproject.toml's requires-python.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 17:39:49 +02:00
Estrella Pan
9fafaef494 feat(backend): add unauthenticated /health liveness endpoint
Add GET /health mounted outside the /api prefix so it bypasses
per-route auth and is reachable as a plain liveness/readiness probe.
Returns {status, version, db_ok}, degrading to db_ok=false with a 200
(not a raised exception) on database failure so the probe stays useful
even when the DB is unreachable. Add a Docker HEALTHCHECK that curls it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 17:39:39 +02:00
Estrella Pan
d9f41f4b11 refactor(backend): route config reload through AppContext, fix blocking I/O, dedupe N+1 query
Setup wizard and config reload now funnel through a single
AppContext.reload_settings() entry point instead of mutating settings
directly. Wrap remaining sync file I/O (image cache, settings load/save,
log tail read) in asyncio.to_thread so async request handlers stop
blocking the event loop. Add reset_cache() to each cache-owning parser/
searcher module (tmdb, mikan, title parser, poster cache) and wire them
into reload_settings() so a base-URL/API-key config change invalidates
stale cached results immediately. Fix BangumiDatabase.add_all's N+1 query
by batching semantic-duplicate candidate lookups into one SELECT for the
whole batch instead of one query per candidate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 17:39:29 +02:00
Estrella Pan
42418d5b18 ci: restore release build fix, gate dev-branch PRs, mock the live TMDB test
- Apply the docker-actions/cache fix from 4a513df so 3.3 releases build; fix buildx outputs typo
- Run CI on PRs targeting 3.*-dev; add ruff + frontend lint/vitest steps
- .dockerignore excludes tests and __pycache__ from the image
- test_tmdb mocks the HTTP layer (was hitting the live TMDB API in the default suite)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
7733bd18f7 fix(webui): setup loop, reactive selects, escape handler, axios/i18n and dep bumps
- Clear needSetup after completion so the wizard stops bouncing back to itself
- ab-select watches modelValue (config selects showed stale defaults); notification-type dropdown renders options
- Search modal Escape handler no longer opens the modal from anywhere; ab-button defaults type=button (no double login)
- axios surfaces 400/404/422; background polls suppress the network-error toast storm; calendar re-reads locally on activate
- Search subscribe uses the active provider + i18n; provider settings go through axios; dead search-store code removed
- Bump axios ^1.7 (CVE-2023-45857) and vue-i18n ^9.14.2 (XSS); add missing i18n keys

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
04c913fdf7 fix(notification): send JSON bodies as JSON, gate poster URLs on a public base_url
- Discord/Webhook/Gotify/Bark POST real JSON via RequestURL.post_json (was form-encoded and always rejected)
- Route through a distinct _post_json to avoid the form-encoding post_json shadowed on RequestContent
- Poster fields use settings.notification.base_url when set, otherwise omitted (local paths were never reachable)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
dc786a29e3 fix(downloader): real rename verification, pipe-join move hashes, stop closing shared client
- qB rename verification uses for/else so it actually polls (was returning True after one check)
- move_torrent pipe-joins hashes so qB applies the move (was silently dropped)
- request_url no longer aclose()s the shared client from the request path (killed concurrent in-flight requests); add post_json
- Single-flight qB 403 re-login behind a lock; ref-guard the cached client on teardown
- ab_decorator re-raises after retries and caps delay; collector add/update fixed; renamer offset-lookup failures skip instead of defaulting to 0; rule delete/disable no longer requires a live downloader

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
f00d39b990 fix(rss/parser): repair OpenAI parser, retry failed adds, stop wiping titles
- OpenAI Episode model uses int season/episode (was str -> silently swallowed TypeError on every parse)
- Mark torrents downloaded only on a successful add so transient failures retry
- Mikan no-poster branch keeps the parsed official_title; set bangumi_id for empty-filter matches; set rss_link on aggregate discovery
- TMDB: guard None results, include test flag in cache key, TTL negative results, skip poster on failed fetch
- Empty filter means no filtering; provider config read at call time; OpenAIParser built once; bounded search enrichment/cache

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
4bab9d00c8 fix(core): best-effort poster cache, abort boot on migration failure, non-blocking start
- cache_image/save_image skip None bytes; cross_version caches posters per-item best-effort (one bad URL no longer aborts boot)
- run_migrations re-raises after rollback so boot aborts on a half-migrated schema; run migrations before legacy data_migration
- AppContext.start_tasks() is non-blocking; startup task is cancelled+awaited on shutdown
- Settings.save/init create config/ dir; robust version.info parse; AB_SOCKS writes proxy type "socks5"
- checker delegates downloader health to the client (aria2 can pass); add webauthn/notification settings fields

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:34 +02:00
Estrella Pan
9f9c7042dc fix(api): convert destructive GETs to POST (CSRF), fix batch routes and N+1
- CSRF: reset/all, enable/disable, log/clear, program start/stop/restart/shutdown, rss refresh are now POST; frontend api calls updated to match
- Batch delete/disable: type list[int], guard empty input, aggregate response, register /many before /{id}
- auto_tag_torrents: load bangumi once and match in memory (was N+1 per torrent)
- Type bangumi_id path params as int; correct 406->404/400; drop false SSE response_model; remove dead imports/handlers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:34 +02:00
Estrella Pan
0f44efa2af fix(security): env-gated auth bypass, WebAuthn hardening, passkey/JWT fixes
- Gate dev auth bypass on AB_DEV_NO_AUTH=1 (was: any non-CI build), warn loudly when on
- WebAuthn rp_id/origin from settings when set; LRU-cap the service cache; match challenge by clientDataJSON
- Passkey: enforce login-IP whitelist, remove user enumeration, stop echoing str(e) in 500s
- Auth: migrate session/JWT on username change; guard decode_token(None); logout is POST + samesite=strict cookie
- Setup: require default admin account before pre-auth /setup/complete
- JWT secret written 0600; move IP-allowlist helper into module/security (new ip_allowlist.py)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:34 +02:00
Estrella Pan
4d7104ca7f chore: bump version to 3.3.0 and add changelog for the architecture refactor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 15:06:42 +02:00
Estrella Pan
5f22d22a9f docs: update CLAUDE.md for the 3.3 async/composition architecture
Refresh the module map and migration instructions, and document the new
conventions (async DB + session-per-operation, constructor-injected
services, downloader Protocol + capabilities, reload_settings) so the old
Program/sub_thread/combine.py references don't mislead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 15:04:43 +02:00
Estrella Pan
526f2bed5d refactor: remove transitional service repo-proxies from Phase 3
Drop the rss/torrent/bangumi @property proxies that RSSEngine and
TorrentManager exposed so the migration could keep the old
Database-is-a-Session call style working. Services now use self.db.<repo>
internally, and call sites reach repos through the Database they already
hold: pure-repo API routes use the injected db directly (dropping a
pointless RSSEngine/TorrentManager construction), and internal callers use
db.<repo> / engine.db.<repo>.

Contract-preserving: the black-box API tests are unchanged; white-box tests
that mocked the service repo-proxies now mock at the Database seam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 15:03:14 +02:00
Estrella Pan
fd47bdcd9a feat(config): configurable TMDB and bgm.tv base URLs (#1040, #1042)
Add a Network config section with tmdb_base_url and bgm_base_url, defaulting
to the official endpoints and read live by the TMDB/bgm.tv parsers. Users
behind a GFW can point them at a reachable mirror without patching code or
restarting. Old config files pick up the defaults automatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 14:47:46 +02:00
Estrella Pan
758cc986bb fix(downloader): build qB rule filter as regex alternation, not char-join
build_rss_rule joined the comma-separated filter string with '|' via
str.join, which splits it into characters ('720,480' -> '7|2|0|,|4|8|0')
instead of alternating whole terms ('720|480') the way every other filter
site does with .replace(',', '|'). Latent since the rule dict was first
written; low real-world impact (the download path uses add_torrent, not
qB-native RSS rules) but the extracted helper is now correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 14:47:22 +02:00
Estrella Pan
939ba5b6ab refactor(database): drop the now-unused sync engine
No production code imports the sync `engine` after the async flip. Remove it
and its foreign_keys connect listener; the migration tests build their own
throwaway sync engine via create_engine and the sync create_tables/
run_migrations helpers in migrations.py, which are kept for that purpose.
DATA_PATH stays only to derive ASYNC_DATA_PATH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 14:35:31 +02:00
Estrella Pan
1cd740657a refactor(database): flip repositories, Database, and all consumers to async
Convert the whole DB stack from a sync Session to AsyncSession in one atomic
change. The async boundary cannot be split into individually-green commits:
async repositories and sync consumers cannot coexist, and the API service-mock
tests break the moment the consumers flip, so the planned repos/consumers/test
commits land together here.

- Repositories (bangumi/rss/torrent/user) take an AsyncSession; every method is
  async. user.py drops sqlmodel session.exec for session.execute().scalars().
- Database is an async context manager over async_session_factory (with an
  optional engine= DI seam for tests); add stays sync, commit/rollback/refresh/
  close and create_table/run_migrations/migrate are async. get_db is async.
- migrations.py gains create_tables_async / run_migrations_async wrappers that
  drive the existing sync-Connection bodies via async_engine.begin() + run_sync.
- Delete the module-level bangumi TTL cache entirely. Its real job — matching
  every torrent in an RSS refresh without re-querying per call — is now done by
  loading the active bangumi list ONCE per refresh cycle in RSSEngine.refresh_rss
  and matching in memory via match_bangumi_in_list().
- Flip every consumer to `async with Database()` + awaits: rss engine/analyser,
  managers (torrent/renamer/collector), core loops/context/offset_scanner,
  notification manager (dropping the asyncio.to_thread poster shim for a direct
  async query), security/api auth helpers, all api routes, mcp tools/resources,
  and the update/* startup chain.
- Fix two pre-existing latent bugs: update/rss.py::update_main_rss and
  update/data_migration.py::data_migration called the async engine.add_rss(...)
  WITHOUT await, so the calls were silently never executed. Both are now awaited
  and their enclosing functions made async up to AppContext.startup.
- RSSEngine/TorrentManager lose their transitional __enter__/__exit__; routes
  construct them plainly. Test fixtures go async (in-memory StaticPool engine;
  bare Database() rebound to a per-test temp-file DB; get_db overridden).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 14:34:17 +02:00
Estrella Pan
6c73226826 refactor(database): enable WAL + busy_timeout on the async engine
Concurrent aiosqlite connections replace the single serialized sync Session,
so add PRAGMA journal_mode=WAL and PRAGMA busy_timeout=5000 to the async
engine's connect listener (keeping foreign_keys=ON). Extract the listener as
_set_sqlite_pragmas_async so tests can attach it to a throwaway engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 13:50:10 +02:00