430 Commits

Author SHA1 Message Date
Estrella Pan
bccda1186a chore: bump version to 3.3.2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 22:16:29 +02:00
Estrella Pan
96e33619f2 fix(downloader): confirm .torrent-file duplicates by infohash on qB <=5.1
qB <=5.1 answers every rejected add with an opaque 200 "Fails.". For
magnet adds we already confirm "actually a duplicate" via the btih
hash, but Mikan torrents are uploaded as .torrent files, so the check
never matched and the add was classified FAILED — and since failed
adds are deliberately not persisted (kept for retry), the same
torrents error-looped every RSS tick ("Fails." ... "no matching
torrent found") forever, e.g. after purging orphan records while the
torrents still exist in qB.

Compute the v1 infohash (SHA-1 of the bencoded info dict, minimal
cursor — no new dependency) from the uploaded bytes and confirm
existence in qB; corrupt files still fail loudly and keep retrying.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 22:09:52 +02:00
Estrella Pan
f0866e6dc9 chore: bump version to 3.3.1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 18:55:18 +02:00
Estrella Pan
1c6fdb85c0 fix(rss): restore disabled rules on explicit re-subscribe/collect
Codex review follow-up: a soft-deleted exact duplicate still orphaned
torrents (add() refuses the insert, the deleted row was refused for
linking, and download proceeded with no id) and the collect path
could link torrents to a still-disabled row. Explicitly subscribing
or collecting a disabled rule can only mean the user wants it back:
restore_one() re-enables the row (keeping its tuned config) and
torrents link to it. eps_complete is unaffected (not_complete already
filters deleted rows).

Also document the ab:renamed tag semantics (top-level media files;
subtitles follow in the same tick, nested extras never rename).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 18:53:43 +02:00
Estrella Pan
86b170de92 fix: address code-review findings on the T1 batch
- renamer: drop retroactive sanitization in gen_path — titles parsed
  from on-disk files are already legal; sanitizing them would batch-
  rename existing libraries containing ":" on upgrade. Sanitization
  stays in gen_save_path (new rules only), with an "Unknown Bangumi"
  fallback when a title is all reserved characters.
- renamer: tag failures no longer break the rename flow (notification
  still returned, cycle continues); collections with unparseable
  media files are not marked ab:renamed.
- collector: duplicate resolution no longer clobbers user-tuned
  fields (eps_collect is set via a targeted update); failed collects
  roll back the freshly inserted row; soft-deleted rules are never
  linked; stale ids pointing at removed rows are cleared.
- database: find_duplicate is deterministic (oldest row) and warns on
  legacy duplicate keys.
- api: auto_tag_torrents matches ab:<digits> exactly so the
  ab:renamed marker doesn't block re-linking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 18:44:49 +02:00
Estrella Pan
3b24b4c6a0 fix(rss): resolve existing bangumi id when re-subscribing a duplicate
bangumi.add() returns False for exact (title_raw+group_name) and
semantic duplicates without backfilling the id, so re-subscribing an
already-known bangumi still orphaned every torrent (found in live
verification against the seeded dev stack). Add find_duplicate() to
the repo and resolve the existing row's id in subscribe/collect
before downloading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 18:23:11 +02:00
Estrella Pan
618464a450 feat(config): user-configurable TMDB API key + Network settings panel (#975, #1042)
Backend: add network.tmdb_api_key (empty = fall back to the built-in
shared key, read live like tmdb_base_url); the config API masks it
automatically.

WebUI: the network section existed backend-only since #1042 — expose
it as a Network settings panel (TMDB API URL, TMDB API key,
Bangumi API URL) with types, initConfig defaults, i18n, and mocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 18:05:53 +02:00
Estrella Pan
e002806bf6 feat(renamer): tag processed torrents with ab:renamed (#147)
After a torrent's media files are renamed (or already conform to the
target naming), add an ab:renamed tag in the downloader so external
scripts (filebot, hlink, ...) can filter tasks AutoBangumi has
finished processing. Skips the API call when the tag is already
present, skips failed renames, and skips the none/normal
pass-through methods. Capability-guarded via the facade, so
aria2/mock downloaders are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 17:59:20 +02:00
Estrella Pan
92ad33b2e4 fix(renamer): keep fractional episode numbers like 12.5 (#667, #704)
The filename parser already yields 12.5 as a float, but gen_path and
rename_file int()-floored it to 12, overwriting the real episode 12.
Extract the duplicated offset logic into _adjust_episode and add
_format_episode (fraction preserved, integer part zero-padded:
"09.5"); widen Notification.episode to int | float so notifications
carry the fraction too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 17:53:29 +02:00
Estrella Pan
bbc91d89f2 fix(renamer): sanitize reserved characters in generated paths (#721)
Titles containing Windows/qB reserved characters (e.g. "Fate/Zero")
were joined raw into save paths and filenames, letting qBittorrent
split them into extra directories or silently truncate. Replace
<>:"/\|?* and control chars with a space in the save-path folder and
in every rename method's title component; collapse whitespace and
strip trailing dots/spaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 17:50:25 +02:00
Estrella Pan
64cfc1b1f8 feat(notification): support Server酱³ sendkeys (#904)
sctp<uid>t-prefixed sendkeys route to the Server酱³ endpoint
(https://<uid>.push.ft07.com/send/<key>.send); Turbo keys keep the
legacy sctapi.ftqq.com endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 17:46:26 +02:00
Estrella Pan
9b194b1af6 fix(rss): link manually added torrents to their bangumi
Torrents added via search/subscribe/collect were persisted with
bangumi_id=NULL, so every matched torrent from those flows landed in
the orphan list regardless of the track_orphans setting, and the
ab:<id> qB tag was silently skipped (bangumi.id did not exist yet).

- subscribe_season/collect_season: insert the bangumi row before
  downloading so its id exists for the qB tag and the FK
- collect_season/download_bangumi: set torrent.bangumi_id before
  persisting

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 17:44:58 +02:00
Estrella Pan
18f5fab9db chore: bump version to 3.3.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 14:24:06 +02:00
Estrella Pan
5fdc2e723f fix: address code-review findings on #1052/#1053 fixes
- Disable (not delete) orphaned per-sub RSS feeds on bangumi deletion:
  a hard delete could not distinguish search-created subscriptions from
  user-added non-aggregate feeds, and RSSDatabase.delete's cascade also
  wiped torrent dedupe records that other bangumi may rely on. Disabled
  feeds keep their history and can be re-enabled; batch-disabled in one
  commit via disable_batch.
- Never remap known parser types (mikan/tmdb/parser) through the
  search-provider config in /rss/subscribe; only genuine site names
  (nyaa/dmhy) are mapped. Prevents parser='mikan' from being rewritten
  when the user customized the mikan provider's parser.
- RSSDatabase.search_url tolerates duplicate-URL rows (first() instead
  of scalar_one_or_none) - url has no DB unique constraint.
- _retry_after_delay rejects NaN Retry-After values, which would
  propagate through min/max into asyncio.sleep(nan) and never wake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 13:02:32 +02:00
Estrella Pan
63821917a5 fix(downloader): throttle batch torrent fetches and back off on HTTP 429 (#1052)
Batch-collecting a season fired all .torrent file downloads at once via
an unbounded asyncio.gather, tripping nyaa's rate limiter; get_url then
gave up on the first 429 with no retry, so the whole batch silently
failed.

- add_torrent now fetches torrent files serially per host with a 1s
  delay between requests (parallel across hosts), mirroring the
  RSS_PER_HOST_DELAY pattern from #1026
- get_url retries 429 responses within the existing retry budget,
  honoring a numeric Retry-After header (clamped to 60s) and falling
  back to a 10s delay otherwise

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 12:42:35 +02:00
Estrella Pan
8f91899670 fix(manager): remove orphaned per-sub RSS feeds when deleting a bangumi (#1053)
Deleting a bangumi left its individually-subscribed (aggregate=False)
RSSItem enabled, so the WebUI kept showing the feed as active and the
RSS loop kept polling it. delete_rule now removes per-sub feeds that no
remaining bangumi references, splitting comma-joined rss_link values and
matching URLs exactly. Aggregate feeds and feeds still referenced by any
bangumi (including soft-disabled ones) are never touched.

Adds RSSDatabase.search_url for exact-URL lookup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 12:42:35 +02:00
Estrella Pan
b659795288 fix(rss): map search-site name to its configured parser on subscribe (#1053)
Search-subscribe sent the provider site name (nyaa/dmhy) as the RSSItem
parser, but the analyser only understands mikan/tmdb, so those
subscriptions silently skipped TMDB title/poster enrichment. Resolve the
site name through the search-provider config at the subscribe boundary;
values that are already parser types pass through unchanged.

Note: the WebUI RSS tag for search-subscribed nyaa/dmhy feeds now reads
"tmdb" (the parser type) instead of the site name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 12:42:35 +02:00
Estrella Pan
1b928ee326 feat(rss): setting to disable orphan-torrent tracking
New bangumi_manage.track_orphans toggle (default on = current
behavior). When off, refresh_rss no longer persists unmatched torrents
(bangumi_id NULL orphan records): they are re-matched in memory each
tick (cheap, no network — the analyser dedups via bangumi rules, not
torrent records), and a rule added later can retroactively pick up
episodes still in the feed, which persisted orphans used to block
forever. Season-collect flows still persist their NULL records for
feed dedup and are not gated.

WebUI: "Track Unmatched Torrents" switch in Settings -> Manage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 12:15:06 +02:00
Estrella Pan
f2875d9870 chore: release 3.3.0-beta.8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 11:38:00 +02:00
Estrella Pan
3caefb5705 Merge feat/torrent-management: bangumi/orphan torrent management (#1051)
3.3 port of #1020 (orphan-torrent DB methods, 7 API endpoints, torrent
management pages), rebased onto 3.3-dev and reworked to the Soft Ink
ab-* component system per code review. Merged locally because the
rebased branch could not be force-pushed to the PR head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 11:34:47 +02:00
Estrella Pan
b887f1833c fix(log): keep log history across restarts and rotation
The log page intermittently showed nothing because the file lifecycle
destroyed history at both ends:

- setup_logger(reset=True) deleted log.txt on every process start
  (container restart, online update), wiping history exactly when the
  user wants to inspect what happened before the restart. Boot now
  rotates the previous run into log.txt.1-3 via the stdlib
  RotatingFileHandler.doRollover (delay=True, rename-only), matching
  runtime rotation naming; failures warn instead of aborting boot.
- At rotation, the UI collapsed to a near-empty log.txt: both readers
  (GET /log and the SSE log event) only read log.txt. The tail read
  now stitches log.txt.1 in front when the current file fits inside
  the 512 KB budget — and never when the read was truncated, which
  would fake a time-jump above a silent gap. A file renamed mid-read
  (rollover race) reads as empty instead of raising, which previously
  could kill the whole SSE stream.
- Rotation cap tightened to 2 MB x 3 backups (~8 MB total); the UI
  renders at most 512 KB so nothing visible is lost.
- POST /log/clear also removes rotated backups (stitching would
  resurrect them) and runs its file I/O in a thread like get_log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 11:32:53 +02:00
Estrella Pan
7c1a7938b1 fix(types): annotate CursorResult.rowcount for mypy in delete_orphans
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-07 10:48:07 +02:00
Estrella Pan
679bd2621c feat: manage bangumi torrents and orphan torrents (port of #1020)
Port zhushanwen321's torrent-management feature (#1020) from the 3.2 sync
architecture to the 3.3 async design.

Backend:
- TorrentDatabase: search_orphans / count_orphans / delete_orphans / delete_obj
- bangumi API: list/delete endpoints for per-bangumi torrents and orphan
  torrents (bangumi_id IS NULL); literal /torrents/orphans routes registered
  before /{bangumi_id}/torrents

WebUI:
- Others card on the bangumi list with orphan count badge
- Torrent list pages (per-bangumi and orphans) with multi-select,
  batch delete, and clear-all confirm dialog

The PR's mikan_parser and rss/analyser fixes are not ported: 3.3-dev
already handles failed poster downloads and missing elements.

Co-authored-by: zhushanwen321 <zhushanwen321@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-07 10:48:07 +02:00
Estrella Pan
a9839e0725 Merge pull request #1056 from EstrellaXD/feat/nexusphp-pt-provider
feat(webui): NexusPHP PT-site preset for search providers
2026-07-07 10:45:52 +02:00
Estrella Pan
fd2dccac4d Merge pull request #1055 from EstrellaXD/perf/slim-docker-image
perf(docker): shrink image 208MB -> 160MB
2026-07-07 10:44:51 +02:00
Estrella Pan
028a0420d5 fix(downloader): qBittorrent 5.x WebAPI compatibility
Verified against qB source at release tags 4.6.7/5.0.0/5.1.0/5.2.0:

- torrents/add: accept qB 5.2 JSON response (200/202) — partial success
  and pending URL fetches count as ADDED, matching legacy "Ok."
  semantics; 409 Conflict is DUPLICATE for synchronous file uploads and
  hash-confirmed magnets, otherwise raised for retry. Send both paused
  and stopped form params (renamed in 5.0, each era ignores the other).
- torrents/pause|resume: renamed to stop|start in qB 5.0 with no
  aliases; try the modern name first, fall back on 404, remember which
  era worked.
- torrents/info: filter=paused was renamed and unknown filter values
  silently mean All — filter paused/stopped client-side by state.
- torrents/delete, torrents/renameFile: treat any 2xx as success
  (qB 5.2 returns 204 for empty-body responses).
- webui: map qB 5.0+ stoppedDL/stoppedUP states to the paused label.
- .gitignore: exclude local runtime data (backend/data), agent
  workspace (.claude), and task notes (tasks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 10:22:27 +02:00
Estrella Pan
378acc90cb feat(webui): NexusPHP PT-site preset for search providers
Add PT (private-tracker) support via the existing RSS-based search
pipeline. The Add Search Provider dialog gains a "PT Site (NexusPHP)"
mode that builds a torrentrss.php search template from base URL +
passkey + optional category ID (e.g. Audiences.me anime = 402), so
search -> subscribe -> auto-download -> rename works end-to-end on
NexusPHP trackers with no backend changes.

- webui/src/utils/nexusphp.ts: URL-template builder (vitest-covered)
- config-search-provider.vue: segmented custom/NexusPHP add dialog
  with live URL preview; en + zh-CN strings
- backend test_nexusphp_rss.py: regression tests proving NexusPHP
  torrentrss output (enclosure and linktype=dl variants) parses
  through the generic rss_parser path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 10:19:26 +02:00
Estrella Pan
c1a0cf8559 perf(docker): shrink image 208MB -> 160MB
Two changes, verified with local builds:

- Drop UV_COMPILE_BYTECODE=1: shipped .pyc files were 51MB. The venv is
  root-owned and the app runs long-lived as 'ab', so bytecode only saved
  a few seconds of import time once per container start. This also
  matches the boot_overlay uv-sync path, which never compiled bytecode.
- mcp[cli] -> mcp: the app only imports mcp.server; the cli extra pulled
  typer/rich/pygments/markdown-it (~14MB) for an unused console script.
  python-dotenv, the extra's other member, is already a direct dep.

Compressed pull size drops 64.2MB -> 48.8MB. Image verified: packages
absent, no __pycache__, 'import main' succeeds; pytest and mypy green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEnmRvBJL93fAURbRf1koK
2026-07-07 10:13:51 +02:00
Estrella Pan
c19da52497 chore: release 3.3.0-beta.7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 16:38:20 +02:00
Estrella Pan
f14ab65574 fix: address stable-review findings across config, update, downloader
Ship-readiness review of main...3.3-dev (multi-agent + codex verify pass)
surfaced 10 confirmed defects; all fixed with red-first tests (+24):

- config: masked list secrets restored by identity match, not index —
  deleting/reordering a notification provider no longer corrupts the
  survivor's credentials; unresolvable masks return 400 instead of guessing
- statics: mount dist/fonts — self-hosted Inter 404'd in every production
  build and the whole UI fell back to OS fonts
- boot_overlay: chown replaced trees to the app user (crash-loop under
  UMASK=077); snapshot-and-restore around the EXDEV in-place fallback so a
  mid-copy failure can't gut /app/module
- updater: rollback refuses to swap without the retained signed backup
  bundle — reverts to image version honestly instead of half-swapping
- qbittorrent: credential-failure latch (cleared on settings save) plus
  single-flighted auth sharing the negative result with queued waiters —
  failed logins no longer accumulate into a WebUI IP ban
- download_client: holder refcount registered before auth (no mid-login
  close), stale clients accumulated in a list (no pool leak on rapid
  settings saves), holder released on cancellation during __aenter__
- entrypoint: ownership marker written only after chown -R succeeds
- aria2: torrents_info provides num_seeds/num_leechs/eta/added_on and maps
  states to qB vocabulary (UI showed undefined / NaNhNaNm / raw states)
- program API: deprecated GET aliases for /restart /start /stop /shutdown
  (3.2 automation compatibility)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 16:37:28 +02:00
Estrella Pan
f751d1c6e7 chore: release 3.3.0-beta.6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 12:43:25 +02:00
Estrella Pan
c96a5dbbeb fix(types): satisfy mypy on the new notification/LLM code
CI runs `mypy src` (not part of the local pytest/ruff/black gate); the
feature commit tripped 22 type errors. No behaviour change:

- events.py: rename the SSE notification payload local so it no longer
  collides with the str-typed `payload` earlier in the generator.
- inbox.py: type: ignore on the `update().where(read == False)` DML
  predicate and the CursorResult.rowcount reads.
- tests: narrow Optional returns (a typed `_upsert` helper for
  InboxDatabase, `assert ... is not None` for credential loads) and
  annotate a bare dict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 09:55:31 +02:00
Estrella Pan
e6d39e5624 feat: in-app notification center and LLM provider plugin framework
Two 3.3 platform features sharing core infra (notification events, DB
migrations/wiring, config model, API registration), committed together
because they cannot be split at file granularity without broken
intermediate builds.

Notification center:
- New `inboxmessage` table (migration 16) + repo with coalescing
  (unread rows with the same dedup_key increment a count; `once` kinds
  like update_available notify per-version) and 500-row pruning.
- SystemEvent contract (kind/severity/dedup_key/payload) with new
  DownloaderUnavailable / UpdateAvailable / UpdateApplied events;
  NotificationManager.send_event persists to the inbox first, then
  broadcasts to external providers only when notification.enable.
- SSE `notification` frames driven by an in-process revision counter;
  REST endpoints for list/unread-count/read/read-all/delete/clear.
- Wire-ups: qBittorrent auth now reports last_auth_error
  (credentials|banned|unreachable) and stops retrying rejected
  credentials to avoid a WebUI IP ban; the startup wait loop aborts
  early on credential errors and emits a notification; a 24h
  update-check tick surfaces new versions; apply/rollback emit results.
- WebUI bell + unread badge + dropdown inbox (severity, coalesce count,
  relative time, deep links), SSE-driven with a 30s poll fallback.

LLM provider plugin framework + subscription auth:
- LLMParser refactored to a thin facade over a provider adapter
  contract + registry; the three built-ins (openai/anthropic/gemini)
  move to adapters with zero behaviour change (existing tests unchanged).
- base_url presets for zai/minimax/deepseek/kimi (mainland + overseas)
  reusing the OpenAI-compatible adapter, with a json_object structured-
  output fallback for providers without json_schema.
- Downloadable signed plugins: catalog + installer clone the update
  system's ed25519 verify / zip-slip-guarded extract pipeline (unsigned
  refused, catalog signed against downgrade); versioned-module-name
  loader hot-loads without restart.
- Subscription auth: `llmcredential` table (migration 17) stores OAuth
  tokens server-side only (never sent to the frontend); device-flow
  auth runs a server-side polling task keyed by an opaque handle so the
  device_code stays off the wire; AuthExpiredError trips the breaker and
  notifies. Two grey-area device-flow plugins (github-copilot,
  codex-chatgpt) under plugins/, flagged at-your-own-risk.
- WebUI: grouped provider select (built-in/preset/subscription/
  downloadable), install with risk confirmation, device-flow connect
  dialog, connected-account chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 09:43:45 +02:00
Estrella Pan
2cdf146f91 fix(bangumi): inherit year for same-title entries across subtitle groups
When a new bangumi entry is created for an already-known show (same
official_title, different group/quality so no semantic merge), it now
inherits the existing entry's year. The save path is built as
`title (year)/Season N`, so a Mikan-parsed entry (which never fills
year) previously diverged into a separate library folder from a
TMDB-parsed sibling that had one. `_inherit_year` in add()/add_all()
copies the year from any non-deleted same-title row when the incoming
entry has none; a parsed year is never overwritten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 09:42:58 +02:00
Estrella Pan
1dc1eb01b4 chore: release 3.3.0-beta.5
Bump version, uv lock, and CHANGELOG for 3.3.0-beta.5 — ships the
overlayfs EXDEV fix for in-app updates (needs a fresh image to take
effect) and moves the Software Update card into Settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-05 12:31:12 +02:00
Estrella Pan
17a3805974 fix(update): survive EXDEV when overlaying the module tree on overlayfs
In-app updates verified and staged fine but silently stayed on the old
version: boot_overlay swaps the code with os.rename('/app/module',
'/app/module.ab_old'), and /app/module comes from the image's overlayfs
lower layer. Some overlay/kernel combos refuse to rename a lower-layer
directory across the mount boundary and raise EXDEV (Errno 18), so the
swap failed on every boot ("Applying overlay ... / Failed to overlay
module tree: Cross-device link") while apply reported success — the user
just saw the frontend restart watch time out.

_replace_tree now catches EXDEV (dst is untouched, rename is atomic) and
falls back to replacing the directory's contents in place, which triggers
overlayfs copy-up per file and works regardless of mount/overlay topology.
Same fix covers the dist tree. Reproduced with a test that forces EXDEV on
the module/dist rename and asserts the overlay still lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-05 12:28:29 +02:00
Estrella Pan
685711d927 chore: release 3.3.0-beta.4
Bump version, uv lock, and CHANGELOG for 3.3.0-beta.4 — bundles the login
redirect fixes, the container-startup optimizations, and the update-check
feedback improvement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-05 09:42:42 +02:00
Estrella Pan
40f83c476d feat(update): make "check for updates" give real feedback
Clicking "check for updates" felt like a no-op: the only signal was a
static one-line "up to date", already shown from the on-mount auto-check,
and the /check endpoint served a 15-minute cache so repeated clicks did
nothing server-side.

- Expose `force` on GET /update/check; the button passes force=true to
  bypass the cache and re-fetch, while the on-mount auto-check stays cached
  (no extra GitHub calls). Updater-level force was already implemented and
  is now locked by a test.
- Active feedback on a user-initiated check: a success toast when already
  current, an info toast naming the new version when one is found, and a
  "Last checked HH:MM:SS" line that updates every click. The check-failed
  toast now includes the backend's reason (rate limit / no release) instead
  of a generic message.

Verified end-to-end: the endpoint reaches GitHub and finds the latest
release (beta.3, whose signed update bundle is attached), so in-app update
is functional; the timestamp advances and the toast fires on each click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-05 09:39:09 +02:00
Estrella Pan
6f72cfe1df perf(startup): defer LLM SDK imports and gate the volume chown
Two things dominated container start:

- The LLM SDKs (anthropic + openai + google-genai) imported at module load
  via parser.analyser/__init__ → llm.py, costing ~0.7s on every boot even
  though the LLM parser is off by default. Move those three heavy imports
  into the provider branch of __init__ and each _parse_* method, so they
  load only when an LLMParser is actually built. Measured: importing
  parser.analyser drops 782ms → 203ms; none of the SDKs are in sys.modules
  after the startup import chain. httpx/httpx_socks stay module-level (cheap,
  and the proxy tests patch them by module path).

- entrypoint.sh ran `chown -R /app/data /app/config` unconditionally every
  start — O(files), which a large poster cache on a NAS turns into the
  dominant startup cost. Walk the tree only when ownership needs fixing
  (first run, changed PUID/PGID, or a wrong-owned volume root), tracked by a
  marker; the legacy bangumi.json migration chowns its one file explicitly so
  the gate can't miss it. /home/ab (image-internal, tiny) is still chowned
  every time.

Also regenerates the auto-imports dts for onResponseError (exported for the
401 test in the previous commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-04 19:16:49 +02:00
Estrella Pan
9a0347cb95 chore: release 3.3.0-beta.3
Bump version, uv lock, and CHANGELOG for the 3.3.0-beta.3 beta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-04 07:42:10 +02:00
Estrella Pan
620b4f5ecc feat(webui): config/log redesign, log dedup, 3.3 UI alignment, LLM model list
Config page redesigned to a section-rail layout (search + per-section
dirty dots, single column), a save bar that names the restart and
double-confirms save/discard, and a navigate-away guard. Log page
redesigned with a health strip (+ jump-to-first-problem), a unified
toolbar (level chips with counts + text search), severity-only row
colour (fixes the dark-mode 3.53:1 contrast failure), an extracted
module column, and overflow-wrap word handling.

Log deduplication: fix the queue log handler using Python's default
formatter (source of the "INFO::module.x:" duplicate prefix on every
line), strip 245 hand-written [Tag] prefixes, drop double-logged
offset-scan/calendar completion lines, demote "Config loaded" to DEBUG.

3.3 feature/UI alignment: aria2 selectable in Config + wizard (with an
aria2 JSON-RPC connection test), preferred group/resolution and
episode_type surfaced in the rule editor (migrations v10/v12), update
auto_check switch with immediate persistence (no restart).

LLM: model input is now a searchable/free-text select that fetches the
provider's model list on demand (new POST /api/v1/config/llm/models);
timeout/cache/concurrency/circuit-breaker tuning fields; default model
gpt-4o-mini -> gpt-5-mini (discontinued).

Accessibility: real <label for> on config controls, aria-labels on
switches/selects, aria-pressed on log chips, 44px touch targets.
Unify delete confirms to NPopconfirm; password inputs; copy fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-04 07:42:10 +02:00
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
2026-07-03 16:23:38 +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
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