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
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
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
- 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
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
- 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
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
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
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
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
- 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
- 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
- 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
- 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
- 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
- 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
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
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
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
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
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
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
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