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.
The release artifact only zipped backend/src, omitting pyproject.toml and
uv.lock, so local deployments couldn't install dependencies. #994 also
reported the missing requirements.txt after the uv migration in 3.2.0.
Install uv in the release job and generate a production-only
requirements.txt via `uv export --no-dev`, then bundle all three alongside
src in the app zip.
Closes#994Closes#1015
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.
Reduces container startup time on low-spec devices (TrueNAS etc.) by limiting recursive chown to /app/data, /app/config, /home/ab instead of /app.
Closes#969 (related performance).
- #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>
Prevent memory leaks by ensuring the search EventSource connection is
closed when the modal unmounts and setTimeout handles are cleared in
copy-to-clipboard flows across modal components.
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>
- useAuth: replace watcher with explicit router.replace on login/logout
- useMyI18n: create single i18n instance at module level (avoid dupes)
- usePasskey: detect WebAuthn support synchronously (no onMounted)
- main.ts: import i18n from hook module instead of calling composable
- ab-add-rss: hoist useApi composables outside functions to avoid
recreating them on each call
- calendar: prevent duplicate refreshes when already refreshing
- downloader: guard interval polling against stale activation state
- router: only mark setupChecked on successful status check
- log store: stop SSE log updates on logout
- i18n: add missing "edit" translation key (en + zh-CN)
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>