Post-review fixes for the batch-3.2.8 branch:
- auth() and the setup wizard again require a positive success marker on
200 responses ('Ok.' body) so a proxy or non-qB service answering
200 + HTML is not mistaken for a login; 204 stays the qB >= 5.2 success.
- delete_rule no longer wraps a failed torrent deletion into a 200 reply,
and the WebUI torrents-delete endpoint reports the failure instead of
answering 'Torrents deleted' unconditionally.
- The close-on-failed-auth teardown now lives once in
DownloadClient.__aenter__ (client.logout() before raising) instead of
being copy-pasted into each concrete client.
- search_all() records a cache generation before querying and skips the
cache write if an invalidation landed in between, so a stale snapshot
can no longer overwrite a newer invalidation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
OpenAIParser now uses AsyncOpenAI — the previous sync SDK call ran directly
on the event loop (submitting to a ThreadPoolExecutor and immediately
blocking on .result() serialized it anyway). TitleParser.raw_parser and its
callers become async accordingly.
The SPA catch-all route listed dist/ on every request; snapshot it once at
startup. The module-level bangumi TTL cache is now lock-guarded — it is
written from asyncio.to_thread workers in notification paths while the event
loop reads it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
refresh_rss fired every feed at once, so sites with many subscribed feeds
(e.g. nyaa.si) rate-limited the whole batch. Feeds are now grouped by host:
serial with a 2s delay within a host, still parallel across hosts, with the
existing global concurrency cap kept.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
/setup/test-downloader now validates the URL scheme (http/https only) like
test-rss already did, and accepts qBittorrent 5.2's 204 login response.
Raw exception and response detail is no longer echoed back from the
pre-authentication setup endpoints; it goes to the server log only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
torrents_delete received a Python list, which httpx form-encodes as repeated
'hashes' fields that qBittorrent ignores — pipe-join it as the API expects.
Check the response status instead of logging success unconditionally, and
propagate the failure up through delete_torrent to the API ResponseModel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
qBittorrent >= 5.2 answers a successful login with HTTP 204 and an empty
body instead of 200 + 'Ok.', so every login was treated as bad credentials.
Accept both variants and keep 200 + 'Fails.' as a failure.
Also close the httpx.AsyncClient when auth() fails: DownloadClient.__aenter__
raises before __aexit__ can run, so each failed connect leaked a client and
its connection pool (qb and aria2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
Closes#984
Apply httpx.Limits(keepalive_expiry=30, max_connections=10, max_keepalive_connections=5) to the qBittorrent AsyncClient — same recipe as #1018 for the RSS side. Prevents the "Server disconnected" cascade when a proxy / NAS silently reaps idle sockets between renamer cycles.
prefix_process called re.sub(f".{group}.", "", raw) unconditionally. When
group was empty (torrents that don't start with [group]), the pattern
degenerated to ".." and every pair of characters in the title was deleted,
leaving a stub the downstream splitter couldn't turn into title_en/zh/jp.
Guarded the substitution with `if group:`. Titles like
"冰之城墙「氷の城壁」The Ramparts of Ice S01E02 1080p 日英双语-多国字幕"
and "Girls Band Cry S01E05 ..." now parse correctly. Updated #764's test
which had been pinned to the broken behavior.
Closes#1025
Mikanime's mirror (and some CDN-fronted sources) respond with 302 to the
canonical host. httpx AsyncClient defaults to follow_redirects=False, so
raise_for_status surfaced the 302 as an HTTPStatusError and the RSS pull
failed in a retry loop.
Enable follow_redirects=True for every construction of the shared client
(proxy, socks5, and direct branches) via a shared kwargs dict.
Closes#983
When AB runs in a Linux container and qBittorrent runs on a Windows host,
qB returns save_path strings with backslash separators. PurePosixPath
treats those as a single unsplittable segment, so _path_to_bangumi never
matched "Season N" and every non-S1 bangumi collapsed to Season 1.
PureWindowsPath accepts both "\\" and "/" as separators, so using it for
the parsing side handles both layouts. Path generation (_gen_save_path /
_join_path) still uses the platform-conditional Path alias, so AB keeps
emitting native paths for the host it runs on.
Diagnosis and fix credited to the reporter in #1016.
Closes#1016
RSS loop runs every 900s but reuses the same httpx.AsyncClient connection pool, well past server keep-alive expiry (60-120s). Reusing dead sockets produced ConnectTimeout errors over time.
- Configure httpx.Limits with keepalive_expiry=60s so idle connections are dropped proactively
- reset_shared_client() called on retry paths to force a fresh pool after ConnectError
- asyncio.Semaphore(5) in refresh_rss() caps concurrent RSS fetches to avoid tripping site rate limits
Closes#1008
Related #1010, #742, #701
Deleting an RSSItem failed with sqlite3.IntegrityError: FOREIGN KEY constraint failed when child torrents still referenced it, leaving the sidebar entry stuck in the UI.
- delete() now removes referencing torrents in the same transaction before removing the RSSItem
- Adds missing session.rollback() in the exception path
- rss_loop wraps each iteration to skip stale RSSItems deleted mid-loop via API
- Tests enable PRAGMA foreign_keys=ON to match production and gain 5 cascade-delete regression tests
Closes#1010Closes#1017
Startup probe hardcoded http:// ignoring settings.downloader.ssl, causing 5-minute startup delay when qBittorrent is HTTPS-only or redirects http→https.
Now derives the scheme from settings.downloader.ssl.
- #992: title_parser returns None for non-episodic resources instead of crashing
- #1005: BangumiDatabase.search_official_title method exists and works correctly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When raw_parser fails to parse non-episodic resources (movies, collections),
it returns None. Add guard clause in title_parser to skip these gracefully
instead of crashing on attribute access. Downgrade log level from error to
info since this is expected behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an MCP client disconnects, Starlette tries to call the return value
of the handle_sse endpoint as an ASGI response. Since the function had
no return statement it returned None, causing:
TypeError: 'NoneType' object is not callable
The mcp library's own SseServerTransport docstring explicitly documents
this requirement. Fix by adding 'return Response()' and importing
starlette.responses.Response.
The per-bangumi default in Bangumi model already uses \d+-\d+ to filter
batch/collection torrents (e.g. '01-13'). The global RSSParser default
was inconsistently using \d+-\d (one fewer +), which has subtly different
matching behaviour.
Align both defaults to \d+-\d+ for consistency.
BangumiDatabase.search_official_title() was called in
notification/manager.py (_get_poster) but never implemented,
causing a RenameThread crash:
'BangumiDatabase' object has no attribute 'search_official_title'
Add the method as a simple exact-match lookup on Bangumi.official_title,
consistent with the existing search_id / search_rss pattern.
When GET /config/get returns config to the frontend, sensitive fields
(password, token, api_key, secret) are masked as '********'. If the
user changes any non-sensitive setting (e.g. ssl: true → false) and
saves, the frontend sends back the masked placeholder verbatim. The
backend was saving it directly, overwriting the real credential with
the literal string '********', breaking authentication silently.
Add _restore_sensitive() to substitute any '********' placeholder with
the current in-memory value before writing to disk. Non-sensitive
fields and genuinely new values are unaffected.
Fixes#995
- Decouple HTTPS scheme selection from TLS certificate verification:
`verify=False` always, since self-signed certs are the norm for
home-server/NAS/Docker qBittorrent setups
- Bump connect timeout from 3.1s to 5.0s for slow TLS handshakes
- Add actionable error messages when HTTPS connection fails
- Fix `continue` → `break` bug in torrents_rename_file verification loop
- Consolidate json imports to top-level
- Add 31 unit tests for QbDownloader constructor, auth, and error handling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add _fallback_parse() tried when TITLE_RE.match() returns None, using two
regex patterns to extract episode numbers from formats the main regex misses:
- digits before [ bracket (issues #876, #910)
- compound [02(57)] format (issue #773)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
10 tests covering the full bug chain:
- raw_parser misparses leading number as episode
- TitleParser.raw_parser returns None for unparseable titles
- add_title_alias rejects None and empty string
- _get_aliases_list filters null values from JSON
- get_all_title_patterns skips None title_raw
- match_torrent and match_list handle corrupted data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Titles like "29 岁单身冒险家的日常" cause the regex to match the leading
number as episode, leaving title_raw as None. This cascades into storing
null aliases and crashing match_torrent with TypeError.
- Fall back to title_jp when title_en and title_zh are both None
- Return None from raw_parser when no title can be extracted
- Reject None/empty aliases in add_title_alias
- Filter null values from parsed title_aliases JSON
- Skip None title_raw in get_all_title_patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow users to drag bangumi cards from the "Unknown" section into weekday
columns in the calendar view. Manual assignments are locked so calendar
refresh from Bangumi.tv doesn't overwrite them. A reset button lets users
unlock and send cards back to Unknown.
Backend:
- Add weekday_locked field to Bangumi model (migration v9)
- Add PATCH /api/v1/bangumi/{id}/weekday endpoint
- Skip locked items in refresh_calendar()
Frontend:
- Add vuedraggable for smooth drag-and-drop
- Pin indicator and unpin button on manually-assigned cards
- Drop zone highlighting during drag
- i18n strings for drag/pin/unpin
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The BangumiDatabase.search_all() uses a module-level TTL cache that
persists across tests using different in-memory SQLite databases.
This caused test_migrate_preserves_existing_data and test_migrate_idempotent
to return stale cached results (1 bangumi instead of 2).
Add an autouse fixture in conftest.py to clear the cache before and after
each test.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Skip episode offset for episode 0 (specials/OVAs) to prevent overwriting
regular episodes (fixes#977)
- Catch re.PatternError in RSS filter compilation and fall back to literal
matching when user filter contains invalid regex chars (fixes#974)
- Remove Aria2 and Transmission from README supported downloaders list
(addresses #987)
- Add regression tests for issues #974, #976, #977, #986
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Aria2: add stub methods for full duck-typing compatibility
- MockDownloader: add verify parameter to rename_file signature
- DownloadClient: raise ConnectionError on auth failure
- Path: fallback bangumi_name from torrent_name when path is flat
- Renamer: remove unused check_pool and dead compare_ep_version
- Pass torrent_name to _path_to_bangumi for better name resolution
- Remove check_pool unit test (feature removed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Database:
- Add error handling and per-step version tracking in migrations
- Enable SQLite foreign keys via PRAGMA on connect
- Fix SQLAlchemy .is_(None) usage, add session.merge() for detached
- Batch commit for semantic alias merges
- Quote table/field names in fill-null-defaults SQL
- Guard against empty user data in migration
Parsers:
- TMDB: bounded LRU cache (512), asyncio.gather for parallel season
fetches, fix season regex \d -> \d+, null-safe year, fix id shadowing
- Raw parser: re.escape() for group/prefix regex, None guard on match
- OpenAI: handle Pydantic model_dump, catch ValueError
Network:
- Null-safe get_html() return
- Error handling per RSS item in mikan parser
- Progressive retry delays (5/15/45/120/300s) with specific exceptions
- Platform detection via sys.platform instead of path heuristic
- Move filter cache to instance variable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use per-task stop events instead of shared stop_event to prevent
stopping one task from killing all others
- Track running state via _tasks_started flag instead of stop_event
- Add error handling in RSS, rename, scan, and calendar loops
- Make restart() resilient to stop failures (catch and continue)
- Cache downloader status check with 60s TTL
- Fix _startup_done set before start() completes (race condition)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Persist JWT secret to config/.jwt_secret (survives restarts)
- Change active_user from list to dict with timestamps
- Extract username from cookie token instead of list index
- Add SSRF protection (_validate_url) for setup test endpoints
- Mask sensitive config fields (password, api_key, token, secret)
- Add auth guards to notification test endpoints
- Fix path traversal in /posters endpoint using resolved path check
- Add CORS middleware with empty allow_origins
- WebAuthn: add challenge TTL (300s), max capacity (100), cleanup
- Remove hardcoded default password from User model
- Use timezone-aware datetime in passkey models
- Adapt unit tests for active_user dict and cookie-based auth
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
67 tests across 11 phases exercising the full AutoBangumi workflow
against Docker infrastructure (qBittorrent + mock RSS server).
Covers setup wizard, auth, config, RSS CRUD, bangumi, downloader,
program lifecycle, log, search, notification, and credential updates
with both happy paths and error conditions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a bangumi was deleted, its associated Torrent records remained in
the database. This prevented re-downloading the same torrents if the
user re-added the anime, because check_new() deduplicates by URL and
would filter out the orphaned records.
Now delete_rule() removes Torrent records before deleting the Bangumi,
so re-adding the same anime correctly treats those torrents as new.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 13 new test cases across three parser test files:
- raw_parser: Chinese 第二季, 2160p resolution, bracketed Season N, multi-group with Chinese punctuation, English-only title
- torrent_parser: EP format, tc/zh-tw subtitle, no-language subtitle (ValidationError), multi-level path, [NNvN] version suffix
- path_parser: season=2/no-offset, large positive offset, offset yielding exactly Season 1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: Episode 0 incorrectly renamed to E01 (#977)
fix: NoneType error in match_list when title_raw is null (#976)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add NotificationProvider base class with send() and test() methods
- Add NotificationManager for handling multiple providers simultaneously
- Add new providers: Discord, Gotify, Pushover, generic Webhook
- Migrate existing providers (Telegram, Bark, Server Chan, WeChat Work) to new architecture
- Add API endpoints for testing providers (/notification/test, /notification/test-config)
- Auto-migrate legacy single-provider configs to new multi-provider format
- Update WebUI with card-based multi-provider settings UI
- Add test button for each provider in settings
- Generic webhook supports template variables: {{title}}, {{season}}, {{episode}}, {{poster_url}}
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes IndexError when parsing torrent names that don't follow the
standard [Group] format. Now returns empty string instead of crashing.
Fixes#973
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add batch offset lookup to reduce N database connections to 1-3 per cycle
- Add search_by_qb_hashes() and search_ids() for batch queries
- Throttle pending rename cache cleanup to once per minute max
- Use exponential backoff for rename verification (0.1s->0.2s->0.4s)
- Skip verification for subtitle renames to reduce latency
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix qBittorrent rename verification (verify file actually renamed)
- Add pending rename cooldown to prevent spam when rename delayed
- Add torrent tagging API for accurate offset lookup
- Add auto calendar refresh every 24 hours
- Fix frontend error handling (don't logout on server errors)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, the rename log message was printed before checking if the
qBittorrent API call succeeded. This caused log spam when rename
operations failed (e.g., due to 409 conflicts or network errors) since
the same file would be attempted again on the next cycle.
Now the log message is only printed after confirming the rename
succeeded, reducing noise in the logs.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
The warning "Episode offset 0 would result in negative episode" was
misleading and caused log spam. The actual issue was either:
1. Parsed episode was 0 or negative (parsing failure or special episode)
2. A negative offset would make a valid episode negative
Changes:
- Differentiate between parsing issues vs offset issues in log messages
- Use debug level for parsed episode issues (likely special episodes)
- Keep warning level only for actual offset problems
- Include original episode value in warning for better debugging
- Handle edge case where parsed episode is 0 by falling back to 1
Fixes#962
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
- Allow setup in dev mode even if settings differ from defaults
- Add mock downloader type for development testing
- Only check sentinel file in dev mode for need_setup status
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>