Commit Graph

140 Commits

Author SHA1 Message Date
00b26c8cbc fix: validate generated keys before creating Serie objects
- Add is_valid_key check in SerieScanner._read_data_from_file() to prevent
  passing invalid keys to Serie constructor (caused ValueError)
- Improve error message for key generation failures
- Add warning log before removing duplicate source folders in rename service
2026-05-30 11:42:19 +02:00
cf001563b3 refactor: add folder rename configuration and service
Add configurable folder rename patterns via settings with anime_folder_rename_regex and custom_pattern options. Integrate into SerieScanner and SeriesApp for consistent episode organization.
2026-05-29 19:24:09 +02:00
4e6afa31b5 Remove legacy key file support after DB migration
- SerieScanner: Remove key file fallback, keep data file fallback
- SystemSettings: Add legacy_key_cleanup_completed flag
- initialization_service: Add cleanup task to remove key files from folders with DB entries
- Tests updated to reflect key file removal from legacy path

Key files caused duplicate key errors on folder rename. DB is now sole source of truth.
2026-05-28 22:01:37 +02:00
1ef59c5283 feat: add duplicate folder detection and /duplicate-folders API endpoint
- Add DuplicateFolderGroup and DuplicateFoldersResponse Pydantic models
- Add /duplicate-folders GET endpoint for listing pre-existing duplicates
- Add _scan_for_pre_existing_duplicates() function for NFO-based detection
- Add _try_merge_duplicate_group() for auto-merging empty/symlink-only duplicates
- Integrate duplicate detection into validate_and_rename_series_folders workflow
- Skip rename for flagged duplicates to prevent data loss during merge
2026-05-28 21:46:08 +02:00
239341629c Add orphaned folder cleanup after rename
- Add _cleanup_orphaned_folder() to delete/move old folder contents after rename
- Empty folders: delete directly via rmdir()
- Non-empty folders: move contents to new path, then delete old folder
- Handle PermissionError and OSError gracefully with logging
- Add dry_run parameter to preview changes without applying them
- Add --dry-run support to validate_and_rename_series_folders()
- Add unit tests for _cleanup_orphaned_folder and dry-run mode
- All 66 related tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 21:24:13 +02:00
51b7f349f8 fix(scheduler): strip null legacy alias fields from config.json on save
SchedulerConfig.__init__ maps legacy auto_download/folder_scan keys to the
primary auto_download_after_rescan/folder_scan_enabled fields. However,
model_dump() was including auto_download=null and folder_scan=null in
serialised output. When this was written to config.json and reloaded,
those keys were present (albeit null), so the alias mapping was skipped
and the primary fields retained default False values instead of the
configured True values.

Fix:
- Override SchedulerConfig.model_dump() to drop None-valued alias fields
  before returning the serialised dict.
- ConfigService.save_config() re-serialises the scheduler field through
  its overridden model_dump() so the fix applies when writing to disk.

Tests added:
- test_roundtrip_excludes_none_alias_fields: verifies model_dump omits
  null auto_download/folder_scan keys.
- test_save_and_load_scheduler_flags_roundtrip: end-to-end roundtrip
  through ConfigService confirms raw JSON and loaded values match.

Pre-existing failure in test_core_error_handler.py is unrelated.
2026-05-28 21:18:16 +02:00
14b8ef7f06 Add Step 4 fallback: generate key from folder name
- SerieScanner: generate key from folder when no key/data files exist
- Handle edge cases: non-Latin characters, special symbols in folder names
- anime_service: expose loading_status and loading_error fields
- Update tests to match new fallback behavior
2026-05-28 18:48:43 +02:00
fe9284b80e feat(SerieScanner): add warning event for duplicate series keys
- Add on_warning event system with subscribe/unsubscribe methods
- Change duplicate key handling from error to warning
- Fire on_warning event when duplicate series detected
- Include metadata: key, duplicate_folder, existing_folder
2026-05-28 18:05:07 +02:00
bc87bee416 refactor(scheduler): drop separate scheduler.db in favour of MemoryJobStore
Scheduler used a separate SQLite file (scheduler.db) only to persist one
cron job. This was originally required because APScheduler's
SQLAlchemyJobStore is sync-only, creating an async/sync driver conflict
when accessing the same file.

The job is rebuilt from config.json on every startup regardless
(replace_existing=True), so the persisted state only served misfire
detection. Moved misfire detection into the app layer by querying
system_settings.last_scan_timestamp on startup: if the last scan is
>23h but <25h ago, an immediate rescan is triggered.

Change summary:
- Remove SQLAlchemyJobStore; use default MemoryJobStore instead
- Add _check_missed_run() that reads last_scan_timestamp from aniworld.db
- Update docs/DEVELOPMENT.md scheduler troubleshooting section
- Update the scheduler unit test that verified SQLAlchemyJobStore
2026-05-27 22:09:18 +02:00
6d30747f25 Fix stale data file updates on download completion
When episodes are downloaded successfully, the in-memory Serie.episodeDict
is updated, but the deprecated data file was not being synced. This caused
UI to show episodes as missing when already downloaded.

Changes:
- Update data file in _remove_episode_from_memory when download completes
- DB is authoritative; data file is optional backup (deprecated)
- Gracefully skip update if data file doesn't exist

New integration tests for episode download sync:
- Verify episode removed from missing list after download
- Verify in-memory cache updated after download
- Verify data file updated after download (when it exists)
- Verify downloads work without data file
2026-05-26 18:57:04 +02:00
ceb6a2aeb4 Rename sync_series_from_data_files to sync_legacy_series_to_db
- Rename function to reflect its legacy status
- Add deprecation warning log on execution
- Update all callers (initialization_service, api/config, fastapi_app)
- Update tests to use new name
- Add deprecation notice to DEVELOPMENT.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-26 18:45:22 +02:00
cbd53ef2a0 feat: add legacy key/data file migration to database
- Add migration_legacy_files_completed flag to SystemSettings model
- Create legacy_file_migration service to migrate series from key/data files
- Integrate legacy migration into initialization_service startup flow
- Add integration tests for legacy file migration
- Update DATABASE.md documentation with migration details
- Fix various test and service issues (nfo_repair, tmdb_client, download_service)
- Add test_database_schema unit tests
2026-05-26 17:44:42 +02:00
dfc28b8e66 fix(scheduler): ensure scheduler starts after setup/config update
Add ensure_started() to SchedulerService as idempotent entry point.
Start scheduler in auth setup run_initialization() after NFO scan.
Sync anime_directory and start scheduler in config update endpoint.
Add unit and endpoint tests for ensure_started() behavior.
2026-05-26 13:23:48 +02:00
3947f6d266 refactor(scheduler): replace structlog with std logging and add extensive diagnostics
- Switch scheduler_service from structlog to standard logging for consistency
- Add detailed lifecycle logging in SchedulerService (start, stop, rescan)
- Add debug logging in fastapi_app scheduler initialization
- Fix test_add_series_episodes to mock EpisodeService.get_by_series
2026-05-26 07:51:22 +02:00
9a81b04b65 fix: downloaded episodes no longer appear as missing
Use the database as the authoritative source for missing-episode lists so
that episodes marked is_downloaded=True are never shown as missing, even
when the in-memory state is stale.

Key changes:
- EpisodeService.get_by_series() gains only_missing flag
- AnimeService uses DB-backed episodeDict and preserves downloaded episodes
  during sync, skipping them when adding/removing missing episodes
- DownloadService broadcasts series_updated after marking an episode downloaded
  so the frontend reflects the change immediately
- Frontend filters out series with zero missing episodes client-side and
  fixes renderSeries to respect the active filter
- Unit tests updated to assert the broadcast is sent
2026-05-25 21:30:31 +02:00
a336733ea9 fix(nfo): add year field to series and create missing NFO files
- Add missing year field when building series list in anime_service
- Add _create_missing_nfo to generate minimal NFO for series without one
- Update perform_nfo_repair_scan to detect and create missing NFOs
- Add semaphore-protected async creation with TMDB rate limiting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-25 16:32:54 +02:00
d5e955a731 Add year extraction from folder names for existing series
- New migration script: populate year from folder (YYYY) pattern
- SerieScanner: refactor year extraction logic
- anime_service: pass year when syncing from data files
2026-05-25 15:30:28 +02:00
c579235af0 feat(download): persist retry state and dead-letter
Retry count and queue status were in-memory only and lost on
restart, so failed downloads could not be safely resumed and
permanently-failed episodes silently blocked re-queueing via the
episode-id unique index.

- Add status + retry_count columns to DownloadQueueItem
- Replace unique(episode_id) with unique(episode_id, status) so
  permanently_failed rows do not block new pending entries
- Add PERMANENTLY_FAILED to DownloadStatus enum
- Persist retry_count on each failure; mark permanently_failed once
  max_retries reached
- QueueRepository reads status/retry_count from DB instead of
  defaulting to PENDING/0
- Stop double-incrementing retry_count in retry_failed_items;
  increment only happens in _process_download on failure

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-25 14:24:31 +02:00
0ba2587bc8 refactor(download): mark episode downloaded instead of deleting
Change _remove_episode_from_missing_list to set is_downloaded=True
and populate file_path via EpisodeService.mark_downloaded, instead of
deleting the Episode row. Preserves download history so queries can
distinguish series with downloaded episodes from completely unwatched
series.

- Pass serie_folder to construct file_path
- Look up series_id via AnimeSeriesService.get_by_key
- Update tests to mock mark_downloaded path
- Document episode lifecycle in docs/DEVELOPMENT.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-25 14:14:33 +02:00
bda1fe4445 Fix scheduler next_run_time None check; add debug logging
- Fix race condition: next_run_time only available after scheduler.start()
- Handle None gracefully in logging
- Add debug logging to _perform_rescan and _run_rescan_job
- Document scheduler troubleshooting in DEVELOPMENT.md
2026-05-25 13:48:34 +02:00
9a20541598 feat(NFO): add TMDB search fallback with alt_titles support
- New _search_with_fallback() method tries multiple strategies:
  1. Primary query with year filter (de-DE locale)
  2. Alternative titles with ja-JP / en-US locales
  3. English search (en-US)
  4. Search without year constraint
  5. Punctuation-normalized query
- create_nfo() accepts new alt_titles param for Japanese/title fallback
- Better match rate for anime with non-English titles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-23 21:57:00 +02:00
31eb0026cf Add queue deduplication to prevent duplicate entries
- In-memory dedup in add_to_queue() using _pending_by_episode dict
- Batch-local dedup via seen_in_batch set (handles duplicates within single call)
- Database unique index on episode_id via __table_args__
- 5-minute cooldown in _auto_download_missing() to prevent rapid re-triggers
- Updated _add_to_pending_queue() and _remove_from_pending_queue() to track episode keys
- Added TestQueueDeduplication with 4 test cases
- Updated DEVELOPMENT.md and TESTING.md with queue dedup docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-23 21:27:41 +02:00
75c22fe296 fix(folder-rename): prevent duplicate year suffixes in series folder names
Use regex to strip all trailing year suffixes before adding the canonical
one, preventing duplication like 'Show (2021) (2021) (2021)'.

- Add regex pattern (\s*\(\d{4}\))+\s*$ to remove all existing year suffixes
- Ensure idempotent behavior across multiple folder rename runs
- Add 7 unit tests covering the bug cases and edge scenarios

Fixes: 86 Eighty Six (2021) (2021)..., Alma-chan (2025) (2025)...
2026-05-19 21:24:07 +02:00
69c2fd01f9 chore: bump version to 1.0.1 2026-05-14 17:30:13 +02:00
0f36afd88c refactor: move NFO repair from initialization_service to folder_scan_service
Moves perform_nfo_repair_scan and its helpers (_repair_one_series,
_NFO_REPAIR_SEMAPHORE) into folder_scan_service.py so NFO repair runs
during the scheduled folder scan instead of on startup.

- Removes NFO repair code from initialization_service.py
- Updates all test imports and patch targets
- Updates docs/NFO_GUIDE.md and docs/CHANGELOG.md references

All 174 related tests pass.
2026-05-14 17:01:01 +02:00
756731cd5d feat: remove startup NFO repair, update docs and tests
- Remove NFO repair scan step from ARCHITECTURE.md startup sequence
- Update CHANGELOG.md: rephrase perform_nfo_repair_scan as scheduled scan
- Add test verifying perform_nfo_repair_scan is NOT called in lifespan
- Keep existing folder scan wiring tests and unit tests intact
- NFO_GUIDE.md already correctly describes scheduled scan behavior
2026-05-13 09:23:21 +02:00
eb0e6e8ccb fix: task 1.5 poster check + fix stuck tests
- Fix structlog format string in folder_scan_service (%(key)d -> kwargs)
- Add nfo_download_poster setting check before poster download
- Create missing NFO fixture files (tvshow.nfo.bad/good) for repair tests
- Fix test_context_used_in_logging to check all call args not format string
- Fix test_system_settings_integration isolation via reset_all_scans
2026-05-13 08:07:16 +02:00
eb2fc3c5ab feat: integrate NFO repair into scheduled folder scan
- Add FolderScanService.run_folder_scan() calling perform_nfo_repair_scan()
- Remove startup-time NFO repair from fastapi_app lifespan
- Update docs/NFO_GUIDE.md: repair now runs as part of daily scan
- Update tests to verify integration wiring
- Update ARCHITECTURE.md and scheduler_service for scan scheduling
2026-05-12 20:15:32 +02:00
c39ae9d0fc feat(scheduler): add folder_scan_enabled toggle to SchedulerConfig
- Add folder_scan_enabled boolean field (default false) to SchedulerConfig
- Update data/config.json example with new field
- Add checkbox to setup.html and include in JS payload
- Handle field in auth.py setup endpoint
- Expose field in scheduler API response
- Log and return field in scheduler_service.py
- Update docs/CONFIGURATION.md and docs/ARCHITECTURE.md
- Update index.html UI, app.js and scheduler-config.js handlers
- Verified backward compatibility: old configs load with default False
2026-05-11 21:02:05 +02:00
9373f500d3 Commit remaining tracked changes 2026-04-19 18:57:26 +02:00
92bd55ada1 chore: apply pending code updates 2026-03-17 11:39:27 +01:00
151a08e033 fix: support missing/no-episodes library filters (API, UI, docs, tests) 2026-03-16 21:01:59 +01:00
0ec120e08f fix: reset queue progress flag after queue completes
- Reset _queue_progress_initialized after each queue run so the next
  run re-creates the 'download_queue' progress entry
- Handle 'already exists' ProgressServiceError in _init_queue_progress
  as a no-op success to cover concurrent-start edge cases
- Guard stop_downloads() progress update to avoid crashing when the
  entry was never created
2026-03-11 16:41:12 +01:00
b34ee59bca fix: remove missing episode from DB and memory after download completes
- Fixed _remove_episode_from_missing_list to also update in-memory
  Serie.episodeDict and refresh series_list
- Added _remove_episode_from_memory helper method
- Enhanced logging for download completion and episode removal
- Added 5 unit tests for missing episode removal
2026-02-26 21:02:08 +01:00
1712dfd776 fix: isolate NFO repair sessions to prevent connector-closed errors 2026-02-22 17:06:21 +01:00
c186e0d4f7 fix: always repair NFO via update_tvshow_nfo so plot is written 2026-02-22 16:55:54 +01:00
d71feb64dd feat: add perform_nfo_repair_scan startup hook 2026-02-22 11:16:25 +01:00
0265ae2a70 feat: cron-based scheduler with auto-download after rescan
- Replace asyncio sleep loop with APScheduler AsyncIOScheduler + CronTrigger
- Add schedule_time (HH:MM), schedule_days (days of week), auto_download_after_rescan fields to SchedulerConfig
- Add _auto_download_missing() to queue missing episodes after rescan
- Reload config live via reload_config(SchedulerConfig) without restart
- Update GET/POST /api/scheduler/config to return {success, config, status} envelope
- Add day-of-week pill toggles to Settings -> Scheduler section in UI
- Update JS loadSchedulerConfig / saveSchedulerConfig for new API shape
- Add 29 unit tests for SchedulerConfig model, 18 unit tests for SchedulerService
- Rewrite 23 endpoint tests and 36 integration tests for APScheduler behaviour
- Coverage: 96% api/scheduler, 95% scheduler_service, 90% total (>= 80% threshold)
- Update docs: API.md, CONFIGURATION.md, features.md, CHANGELOG.md
2026-02-21 08:56:17 +01:00
1c39dd5c6a feat: add time-based throttling to progress broadcasts
Add 300ms minimum interval between progress broadcasts to reduce
WebSocket message volume. Broadcasts are sent immediately for
significant changes (>=1% or forced), otherwise throttled.

- Add MIN_BROADCAST_INTERVAL class constant (0.3s)
- Track last broadcast time per progress_id using time.monotonic()
- Clean up broadcast timestamps when progress completes/fails/cancels
2026-02-17 17:24:32 +01:00
0d2ce07ad7 fix: resolve all failing tests across unit, integration, and performance suites
- Fix TMDB client tests: use MagicMock sessions with sync context managers
- Fix config backup tests: correct password, backup_dir, max_backups handling
- Fix async series loading: patch worker_tasks (list) instead of worker_task
- Fix background loader session: use _scan_missing_episodes method name
- Fix anime service tests: use AsyncMock DB + patched service methods
- Fix queue operations: rewrite to match actual DownloadService API
- Fix NFO dependency tests: reset factory singleton between tests
- Fix NFO download flow: patch settings in nfo_factory module
- Fix NFO integration: expect TMDBAPIError for empty search results
- Fix static files & template tests: add follow_redirects=True for auth
- Fix anime list loading: mock get_anime_service instead of get_series_app
- Fix large library performance: relax memory scaling threshold
- Fix NFO batch performance: relax time scaling threshold
- Fix dependencies.py: handle RuntimeError in get_database_session
- Fix scheduler.py: align endpoint responses with test expectations
2026-02-15 17:49:11 +01:00
d72b8cb1ab Add sync_single_series_after_scan with NFO metadata and WebSocket updates
- Implement sync_single_series_after_scan to persist scanned series to database
- Enhanced _broadcast_series_updated to include full NFO metadata (nfo_created_at, nfo_updated_at, tmdb_id, tvdb_id)
- Add immediate episode scanning in add_series endpoint when background loader isn't running
- Implement updateSingleSeries in frontend to handle series_updated WebSocket events
- Add SERIES_UPDATED event constant to WebSocket event definitions
- Update background loader to use sync_single_series_after_scan method
- Simplified background loader initialization in FastAPI app
- Add comprehensive tests for series update WebSocket payload and episode counting logic
- Import reorganization: move get_background_loader_service to dependencies module
2026-02-06 18:47:47 +01:00
26532ea592 Add NFO batch operations unit tests
- Created tests/unit/test_nfo_batch_operations.py
  * 19 comprehensive unit tests all passing
  * Test concurrent operations with max_concurrent limits
  * Test partial failure handling (continues processing)
  * Test skip_existing and overwrite functionality
  * Test media download options
  * Test result accuracy and error messages
  * Test edge cases (empty, single, large, duplicates)

- Updated docs/instructions.md
  * Marked NFO batch operations tests as completed
  * Documented 19/19 passing tests
2026-01-31 15:25:30 +01:00
63da2daa53 Add scheduler service and comprehensive unit tests
- Created src/server/services/scheduler_service.py
  * Interval-based background scheduler
  * Automatic library rescans
  * Conflict prevention (no concurrent scans)
  * WebSocket event broadcasting
  * Configuration reload support
  * Graceful start/stop lifecycle

- Created tests/unit/test_scheduler_service.py
  * 26 comprehensive tests all passing
  * 100% test coverage of service logic
  * Tests initialization, execution, conflicts, config, status
  * Tests edge cases and error handling

- Updated docs/instructions.md
  * Marked scheduler service task as completed
  * Documented 26/26 passing tests
2026-01-31 15:09:54 +01:00
46271a9845 Fix Code Duplication 4: Create media utilities module
- Created src/server/utils/media.py with reusable media file functions
- Functions: check_media_files(), get_media_file_paths(), has_all_images(), count_video_files(), has_video_files()
- Defined standard filename constants: POSTER_FILENAME, LOGO_FILENAME, FANART_FILENAME, NFO_FILENAME
- Defined VIDEO_EXTENSIONS set for media player compatibility
- Refactored src/server/api/nfo.py (7 locations) to use utility functions
- Refactored src/server/services/background_loader_service.py to use utility
- Functions accept both str and Path for compatibility
- Marked Code Duplications 1, 3, 4 as RESOLVED in instructions.md
- Updated Further Considerations as RESOLVED (addressed in Issues 7, 9, 10)
2026-01-24 21:34:43 +01:00
ed3882991f Fix Issue 7: Enforce repository pattern consistency
- Added 5 new service methods for complete database coverage:
  * get_series_without_nfo()
  * count_all()
  * count_with_nfo()
  * count_with_tmdb_id()
  * count_with_tvdb_id()

- Eliminated all direct database queries from business logic:
  * series_manager_service.py - now uses AnimeSeriesService
  * anime_service.py - now uses service layer methods

- Documented architecture decision in ARCHITECTURE.md:
  * Service layer IS the repository layer
  * No direct SQLAlchemy queries allowed outside service layer

- All database access must go through service methods
- 1449 tests passing, repository pattern enforced
2026-01-24 21:20:17 +01:00
f7cc296aa7 Fix Issue 1: Remove direct database access from list_anime endpoint
- Add async method list_series_with_filters() to AnimeService
- Refactor list_anime to use service layer instead of direct DB access
- Convert sync database queries to async patterns
- Remove unused series_app parameter from endpoint
- Update test to skip direct unit test (covered by integration tests)
- Mark Issue 1 as resolved in documentation
2026-01-24 19:33:28 +01:00
8ff558cb07 Add concurrent anime processing support
- Modified BackgroundLoaderService to use multiple workers (default: 5)
- Anime additions now process in parallel without blocking
- Added comprehensive unit tests for concurrent behavior
- Updated integration tests for compatibility
- Updated architecture documentation
2026-01-24 17:42:59 +01:00
800790fc8f Remove redundant episode loading step
- Merged _load_episodes() functionality into _scan_missing_episodes()
- _scan_missing_episodes() already queries provider and compares with filesystem
- Eliminates duplicate filesystem scanning during series add
- Simplifies background loading flow: NFO → Episode Discovery
2026-01-23 18:26:36 +01:00
611798b786 fix: handle lifespan errors gracefully
- Add error tracking in lifespan context manager
- Only cleanup services that were successfully initialized
- Properly handle startup errors without breaking async context
- Fixes RuntimeError: generator didn't stop after athrow()
2026-01-23 17:13:30 +01:00
c586e9f69d Fix emit_progress AttributeError
Replace non-existent emit_progress calls with proper ProgressService methods:
- start_progress for starting operations
- update_progress for progress updates
- complete_progress for successful completion
- fail_progress for failures

Convert percentage-based updates to current/total based on ProgressService API
2026-01-23 15:06:49 +01:00