The Anime Settings page (src/server/web/static/js/pages/anime-settings.js)
calls GET /api/nfo/{key}/content from its 'View NFO XML' button, but that
endpoint was removed during the NFO refactor (commits 21af502, a8e5487).
The frontend was never updated, so every click on the button 404'd.
Fix:
- Add NfoContentResponse model (key, folder, content, file_size,
last_modified) to src/server/models/nfo.py.
- Add GET /api/nfo/{key}/content handler to src/server/api/nfo.py that
reads <anime_directory>/<folder>/tvshow.nfo and returns it as
{"content": "<xml>", ...} — matching what anime-settings.js
viewNfoContent() already expects (data.content).
- Expose viewNfoContent on AniWorld.AnimeSettingsManager so it is
consistent with the other public methods and directly callable from
tests / other modules.
Tests:
- tests/api/test_nfo_endpoints.py: 4 new tests (auth-required, happy
path returning XML, 404 on unknown series, 404 on missing tvshow.nfo).
Also remove the file-local autouse 'reset_auth' fixture that wiped
the conftest's master-password setup and made any login-based test
fail with a stale-hash 'invalid credentials' error — that fixture
was pre-existing and is a no-op now that conftest.py handles reset.
- tests/frontend/unit/anime_settings.test.js: 3 new tests for
viewNfoContent (URL + auth header, writes <pre>, error toast on
404) and an assertion in the public-API surface test.
The success path of handleConfirm() never reset the isSubmitting flag,
so hide()'s early-return guard (line 209: 'if (isSubmitting) return')
kept the modal visible after a successful delete — leaving the user
looking at a stuck 'Deleting...' dialog while the card had already been
removed by the WebSocket SERIES_DELETED event.
Reset isSubmitting and the confirm button text before calling hide(),
and capture currentKey into a local before hide() nulls it so the
follow-up removeSeries() call receives the right key.
When release.sh is launched from VS Code's integrated terminal, podman
sees $HOME=/home/$USER/snap/code/<rev>/ and stores its database there.
If snapd auto-updates code mid-session (rev 258 -> 260), the next
podman invocation finds a stale static-dir pointer and aborts with:
Error: database static dir ".../snap/code/258/.../libpod" does not
match our static dir ".../snap/code/260/.../libpod"
Detect the pattern ($HOME under /home/*/snap/*) and bail out early
with an actionable message: re-run from a regular host terminal.
The single-quoted bash string passed to sed -i prevented FRONT_VERSION from
being interpolated, so the file content was being silently rewritten to
the literal text "${FRONT_VERSION}". Likewise the double-quoted sed for
pyproject.toml would silently do nothing if the [project] section was
absent while still printing a misleading 'updated' success line.
Replace both with python: json.load/dump for package.json (so the JSON
remains well-formed and 2-space-indented), and a regex section-aware
edit for pyproject.toml that exits non-zero with a warning if no
`version = "..."` line is found, so the success message only prints
when something actually changed.
When the year-suffixed target folder already exists on disk, both
FolderNamingService and AnimeService.rename_folder_if_needed used to
silently bail out. The bare folder (e.g. 'Ultraman') was left next to
the year-suffixed one ('Ultraman (2019)'), producing the symptom
'reports series like Ultraman as added twice' — the DB has one row but
the filesystem has two folders holding the same content.
Fix: when the target already exists, merge the source's contents into
the target (target version wins on file conflicts; source copies are
removed so cleanup succeeds), remove the now-empty source directory,
update DB row + in-memory cache. Plain rename path is unchanged.
Also fixes a latent TypeError in rename_folder_if_needed where
self._directory (a str) was used with the '/' operator. Production
behavior was that any rename through that method raised and was
swallowed by the caller's try/except, leaving the bare folder
untouched. The new path builds Path objects from the string base.
Tests:
- Replaced test_skips_when_target_folder_already_exists (which
codified the bug) with three tests that cover the new merge
contract: clean merge, no-overwrite, and empty-source removal.
- Added tests/unit/test_rename_folder_if_needed.py covering the
same scenarios plus the str-directory regression. All seven go
red on the unfixed code and green with the fix.
Fixes the 'Ultraman' / 'Ultraman (2019)' duplicate-folder bug.
Two follow-ups to the previous fix that resolved the CWD-relative-path
bug in delete_series:
1. Reorder deletion: filesystem first, database second.
When both delete_database=True and delete_folder=True were set and
the folder delete failed (any reason — path math bug, permission
error, missing volume, etc.), the database row was already gone by
the time the folder delete was attempted. This left an orphan
folder on disk that the user had no normal way to clean up — the
delete UI returns 'Series not found' when the row is gone.
Doing folder delete first means a folder-side failure preserves
the DB row, so the user can retry the delete once the underlying
issue is resolved.
2. Orphan-folder recovery: when the user requests delete_folder=True
for a key whose DB row no longer exists, scan the configured
anime directory for a folder that uniquely matches the key
(normalized: strip trailing (YYYY), lowercase, remove
hyphens/underscores/non-alphanumerics) and delete it. This
recovers the Beyblade Burst scenario: a previous delete attempt
removed the row but the folder stayed on disk; retrying the
delete now cleans up the orphan.
Safety: recovery only triggers when exactly one folder matches
the normalized key. Zero matches → clear 'no folder matching'
error. Multiple matches (ambiguous) → refuses to delete anything.
Adds four regression tests:
- test_delete_series_db_preserved_when_folder_fails
- test_delete_series_orphan_folder_recovery (the Beyblade Burst case)
- test_delete_series_orphan_folder_no_match
- test_delete_series_orphan_folder_ambiguous
The folder column in anime_series stores the relative folder name
(e.g. 'Beyblade Burst (2016)'), not an absolute path. The old
delete_series code called os.path.abspath(folder_path) directly,
which joins a relative path against the process current working
directory. Inside the container the FastAPI app runs with CWD=/app
while the anime directory is /data, so 'Beyblade Burst (2016)'
resolved to '/app/Beyblade Burst (2016)' and the is_safe_path check
correctly (but unhelpfully) flagged it as outside the /data base,
skipping the folder delete while still removing the row.
Fix: resolve relative folder paths against the configured anime
directory before validating against the base. Absolute paths still
work unchanged.
Also harden is_safe_path() the same way so a relative target is
treated as relative to base_path, not to the process CWD. Path
traversal ('../etc/passwd') is still rejected.
Adds two regression tests:
- is_safe_path with chdir to '/' and relative target resolves
against the base
- delete_series with chdir to '/', relative folder in DB,
succeeds and removes the folder
Add null guards and element re-caching in delete-modal.js so the modal
recovers gracefully if its DOM is replaced (e.g. by an HTMX swap) between
init and show().
Also fix the two tests that broke in this environment:
- test_delete_modal.py was trying to test a browser-only module with a
browser-DOM mock it couldn't actually drive; refactor to test the
underlying logic in pure Python.
- test_delete_anime_security.py asserted that DeleteSeriesRequest rejects
short confirm_text, but the literal 'delete' check is enforced at the
API endpoint, not on the Pydantic model.
Prevent fully-downloaded series from being queued by auto-download:
- SerieList.GetMissingEpisode(): filter by is_downloaded instead of
checking if episodeDict is non-empty. episodeDict from the DB
relationship includes all episodes (including downloaded), so a
series with only downloaded episodes still had a truthy episodeDict.
- AnimeSeries.episodeDict property: skip episodes where is_downloaded=True
when building the dict from the DB relationship. This makes the
property consistent with the is_downloaded filtering already done
manually in list_series_with_filters(), and ensures that calling
code anywhere in the codebase gets the correct missing-episode view.
Fixes hana-kimi (and any other fully-downloaded series) incorrectly
appearing in the auto-download queue after a rescan.
SQLAlchemy async sessions are not safe for concurrent operations. load_series_images_batch was using asyncio.gather to run multiple load_series_images calls concurrently, all sharing the same db session. This caused 'session is provisioning a new connection; concurrent operations are not permitted' errors for every series in the batch.
Fix by processing each batch sequentially instead of concurrently. BATCH_SIZE=10 still paces TMDB requests as intended.
When _try_direct_stream() succeeded, it streamed chunks directly via
requests.get() without firing any progress events. This caused the WebSocket
frontend to only see 'started' (0%) and 'completed' (100%) — no incremental
updates.
Add per-chunk progress events to _try_direct_stream that mirror yt-dlp's
hook format (downloaded_bytes, total_bytes, speed, eta, status), emitted at
~1% intervals. These flow through the existing download_progress handler chain
(SeriesApp → anime_service → progress_service → WebSocket → frontend).
The websocket-client strips the outer {type, data, ...} wrapper before
emitting to handlers, so handlers receive the inner series data object
directly (e.g. {key, name, missing_episodes}) — not {type, data: {...}}.
The series_updated handler was checking data.data which always failed,
causing every update to fall back to a full loadSeries() call instead of
calling updateSingleSeries() directly. This prevented the missing
episodes count from updating in real-time after a download completed.
Fix: check data.key directly and pass data (not data.data) to
updateSingleSeries().
Runs after NFO refresh during scheduled rescans. Renames folders that
are missing a year (e.g. 'Naruto' → 'Naruto (1999)') using the year
from the database record.
Safety: _build_target_folder() always strips any existing year suffix
first, preventing double/triple year accumulation like
'Naruto (1999) (1999) (1999)'.
Changes:
- New FolderNamingService (folder_naming_service.py) with safe target
name construction, DB update, and in-memory cache update
- New SchedulerConfig field: folder_naming_after_nfo_scan (default True)
- Integrated as step 3 in scheduler _perform_rescan() after NFO scan
- Runtime UI: existing 'folder-scan-enabled' checkbox in index.html
now wired to toggle the feature (app.js + scheduler-config.js)
- Setup screen: new checkbox in setup.html Scheduler Settings section
- API: scheduler config endpoint returns all scan toggles
- Tests: 39 unit tests covering static helpers, rename logic, safety
guard, and integration cases (folder_naming_service.py)
- Docs: testing guide updated with FolderNamingService examples
- Add 'system_progress' event type to loading page redirect condition
- Add checkUnresolvedAndRedirect() for phase=initial to handle race condition
where backend initialization completes before WebSocket connects
- Backend now emits series_sync progress events during initial setup
- Loading page checks /api/setup/unresolved immediately on load for phase=initial
- Fixes users getting stuck on loading page after setup
- Remove Task 5/6 from docs (tests now fixed)
- Close Settings Modal Via Escape: click focus + sleep before Escape key
- Edit Scheduler Settings: use checkbox-custom selector
- Disable Scheduler: use force=True on checkbox id
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reset was not putting app in unconfigured state. _needs_setup()
was checking is_configured() but not verifying master_password_hash
exists in saved config file. Added explicit check for this key so
reset properly triggers setup form.
Also removed completed Task 26 from docs.
Add Verify App Is Unconfigured keyword with retry logic.
Update Test Setup to reset app state before each setup flow test.
Remove stale Task 25 docs from tasks.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add /api/config/admin/reset unauthenticated endpoint for tests
- Add auth_service.reset() to clear in-memory auth state
- Update robot tests to call reset on teardown
- Remove flaky Setup Redirects test (depended on test order)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add data-testid='toast' to toast element in ui-utils.js
- Update toast selector in ui_keywords.resource to use [data-testid='toast']
- Update login rate limit test to check for 'invalid' instead of 'lockout'
(testing mode disables lockout, so test verifies proper error message)
- Remove completed task docs from tasks.md