- 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>
- 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>
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>
When users have a SOCKS proxy configured (via HTTP_PROXY or ALL_PROXY
environment variables), httpx's AsyncClient automatically tries to use
it but fails without the socksio package installed.
Changed httpx dependency from httpx>=0.25.0 to httpx[socks]>=0.25.0 to
include the socksio package as an extra dependency.
Fixes#961
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>
Pydantic V2:
- Replace @validator with @field_validator in models/config.py
- Replace .dict() with .model_dump() in Config, Settings, and BangumiDatabase
- Replace .parse_obj() with .model_validate() in Settings and tests
- Replace Field(example=) with Field(json_schema_extra=) in response models
Datetime:
- Replace datetime.utcnow() with datetime.now(timezone.utc) in jwt.py
- Update factories.py to use timezone-aware datetime
FastAPI:
- Migrate from deprecated @router.on_event() to lifespan context manager
- Move startup/shutdown handlers from program.py to main.py
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update requires-python to >=3.13 in pyproject.toml
- Update ruff and black target versions to py313
- Update Dockerfile to use python:3.13-alpine
- Add explicit Python 3.13 setup in CI workflow
- Regenerate uv.lock for Python 3.13
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use RSS type instead of non-existent RSSItem/RSSResponse
- Add expire field to mockLoginSuccess
- Replace offset with episode_offset/season_offset in mockBangumiAPI
- Add needs_review_reason field to mockBangumiAPI
- Add missing RSS fields (connection_status, last_checked_at, last_error)
- Fix generic types in test utilities
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backend:
- Add API test files for auth, program, downloader, config, log, bangumi extended, search, and passkey endpoints
- Update conftest.py with new fixtures (app, authed_client, unauthed_client, mock_program, mock_webauthn, mock_download_client)
- Update factories.py with make_config and make_passkey functions
Frontend:
- Setup vitest testing infrastructure with happy-dom environment
- Add test setup file with mocks for axios, router, i18n, localStorage
- Add mock API data for testing
- Add tests for API logic, store logic, hooks, and basic components
- Add @vue/test-utils and happy-dom dev dependencies
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When subtitle groups change their naming convention mid-season (e.g.,
"LoliHouse" → "LoliHouse&动漫国"), AutoBangumi was creating duplicate
entries. This adds a title alias system that:
- Detects semantic duplicates (same official_title, dpi, subtitle,
source, and similar group name)
- Merges them as aliases instead of creating new entries
- Updates match_torrent() and match_list() to check aliases
- Adds title_aliases field to Bangumi model (JSON list)
- Includes migration v8 for the new column
- Adds 10 new tests for the feature
- Fixes cache invalidation bug in disable_rule()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace N individual _is_duplicate() calls with single batch SELECT query
in add_all() method, reducing database round-trips
- Replace O(n*m) nested loop in match_list() with compiled regex alternation
pattern for faster torrent-to-bangumi matching
- Add LRU cache (512 entries) to torrent_parser() to avoid redundant regex
parsing for the same torrent paths
- Extend bangumi search_all() cache TTL from 60s to 300s (5 minutes)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change episode_offset type from int to int | None
- Only set episode_offset when virtual season split is detected
- For simple season mismatches (e.g., RSS S2 → TMDB S1), episode_offset is now None
- Improve reason messages to clarify when episode offset is/isn't needed
- Update database migration version to 7 and add migration check
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When offset scanner detects a mismatch, it now stores:
- suggested_season_offset: recommended season offset value
- suggested_episode_offset: recommended episode offset value
These values are returned in the API response for bangumi that need review,
allowing the frontend to display them to help users configure the correct offset.
Database migration v7 adds the new columns.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When user sets season_offset, the save path now reflects the adjusted season:
- _gen_save_path() uses (season + season_offset) for folder name
- Files saved directly to correct folder (e.g., Season 2 instead of Season 1)
- update_rule() now updates qBittorrent RSS rule's savePath when offset changes
- Existing torrents are moved to the new location
Renamer changes:
- gen_path() no longer double-applies season_offset (folder already has it)
- Season number taken directly from folder name
- Added path normalization for better save_path matching
- Added debug logging for offset lookup
Torrent name matching (title_raw) remains primary fallback for finding bangumi.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When multiple bangumi subscriptions share the same save_path (e.g., split-cour
anime with S01E1-12 and S01E13-24), the renamer's match_by_save_path() query
returned multiple rows causing "Multiple rows were found" errors.
Changes:
- Add qb_hash field to Torrent model for direct hash-to-bangumi linking
- Add database migration v6 for qb_hash column with index
- Add tags parameter to add_torrents() in all downloader clients
- Tag new torrents with ab:{bangumi_id} for offset lookup during rename
- Implement multi-tier lookup in renamer: qb_hash -> tags -> torrent_name -> save_path
- Fix auth tests by mocking DEV_AUTH_BYPASS for proper 401 testing
The renamer now reliably finds the correct bangumi and its offsets even when
multiple subscriptions download to the same directory.
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>
FastAPI's merged_lifespan mechanism triggers lifespan events for each
nested router layer. Since program_router is included in v1, which is
included in app, the startup handler was being called 3 times.
Added _startup_done flag to ensure Program.startup() only executes once.
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>