Files
Aniworld/src/server/web/templates/anime-settings.html
Lukas eabce18e41 feat(anime): rename NFO Diagnostics to Anime Settings + add edit endpoints
Replaces the read-only 'NFO Diagnostics' page with a full per-anime
Settings page reached from the right-click context menu on series cards.
Users can now view and edit key, name, folder, tmdb_id, tvdb_id and site
for each anime; changes are persisted to the DB and optionally written
back to the NFO file or used to regenerate it.

Backend
- Rename NfoDiagnosticsResponse -> NfoSettingsResponse,
  NfoSeriesDiagnostics -> NfoSeriesSettings
- Rename get_nfo_diagnostics -> get_nfo_settings,
  repair_nfo -> repair_nfo_settings
- Fix nfo.py bug: repair was calling non-existent
  update_series_nfo_status(); now uses update_nfo_status() and an
  explicit AnimeSeriesService.update(nfo_path=...)
- New endpoints on /api/anime/{key}:
    GET  /settings            -> AnimeSettingsResponse
    PUT  /settings            -> AnimeSettingsResponse
                                 (body: name/folder/tmdb_id/tvdb_id/site,
                                  options: apply_to_nfo, rename_disk)
    POST /regenerate-nfo      -> AnimeSettingsRegenerateNfoResponse
- New Pydantic models: AnimeSettingsResponse,
  AnimeSettingsUpdateRequest, AnimeSettingsRegenerateNfoResponse
- /anime/settings page route; /settings/nfo now 301-redirects to it

Frontend
- New AniWorld.AnimeSettingsManager JS module (single-page form,
  no tabs) with public API init/loadSeries/saveSettings/regenerateNfo/
  validateField/populateForm/showSaveSuccess/showError
- New anime-settings.html template + anime-settings.css
- Right-click menu: data-action 'nfo-diagnostics' replaced by
  'anime-settings' (label 'Anime Settings'), navigates to
  /anime/settings?key=...
- Library 'Open NFO Diagnostics' link renamed to 'Open Anime Settings'

Bug fix
- context-menu click handler was calling hide() BEFORE building the
  navigation URL, which cleared currentSeriesKey to null and produced
  /anime/settings?key=null. Captures the key into a local const first.
  Regression-locked by tests/frontend/unit/context_menu.test.js.

Tests
- 21 new pytest tests in tests/api/test_anime_settings_endpoints.py
  (GET/PUT/regenerate-nfo, auth, validation, nfo-repair bug regression)
- tests/api/test_nfo_endpoints.py trimmed to 6 focused tests
- 31 new Vitest unit tests for AnimeSettingsManager
- 5 new Vitest unit tests for ContextMenu (incl. source-invariant
  regression guard for the hide()-before-key bug)
- 5 new Playwright E2E tests covering right-click, direct nav,
  legacy /settings/nfo redirect, and context-menu labels
- New vitest.config.js (environment: happy-dom)

Docs
- Docs/API.md: new section 'Anime Settings Endpoints'
- Docs/CHANGELOG.md: documents the rename and the context-menu bug fix

Verified
- pytest: 27/27 (21 new + 6 trimmed nfo)
- vitest: 36/36 (31 anime-settings + 5 context-menu)
- playwright e2e: 5/5
2026-06-21 07:52:22 +02:00

248 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anime Settings - AniWorld Manager</title>
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_version('css/styles.css') }}">
<link rel="stylesheet" href="/static/css/pages/anime-settings.css?v={{ static_version('css/pages/anime-settings.css') }}">
<link rel="stylesheet" href="/static/css/ux_features.css?v={{ static_version('css/ux_features.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="header">
<div class="header-content">
<div class="header-title">
<i class="fas fa-gear"></i>
<h1>Anime Settings</h1>
</div>
<div class="header-actions">
<a href="/" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i>
<span>Back to Library</span>
</a>
</div>
</div>
</header>
<main class="main-content anime-settings-main">
<!-- Series key selector (when no key in URL) -->
<section id="no-key-section" class="settings-section hidden">
<h2>Select a Series</h2>
<p class="config-hint">
No series selected. Right-click any series card on the
library page and choose <strong>Anime Settings</strong>,
or use the dropdown below.
</p>
<div class="config-item">
<label for="series-select">Series:</label>
<select id="series-select" class="input-field">
<option value="">Loading series...</option>
</select>
<button id="load-series-btn" class="btn btn-primary">
<i class="fas fa-folder-open"></i>
<span>Open Settings</span>
</button>
</div>
</section>
<!-- Loading state -->
<section id="loading-section" class="settings-section">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading series settings...</p>
</div>
</section>
<!-- Error state -->
<section id="error-section" class="settings-section hidden">
<div class="error-box">
<i class="fas fa-exclamation-triangle"></i>
<h2>Could not load settings</h2>
<p id="error-message">Unknown error.</p>
<button id="retry-btn" class="btn btn-primary">
<i class="fas fa-rotate"></i>
<span>Retry</span>
</button>
</div>
</section>
<!-- Main settings view -->
<section id="settings-section" class="settings-section hidden">
<!-- Header card with name + status badges -->
<div class="settings-header-card">
<h2 id="series-display-name">Loading...</h2>
<div class="status-badges">
<span id="badge-loading-status" class="status-badge"></span>
<span id="badge-has-nfo" class="status-badge"></span>
<span id="badge-episode-counts" class="status-badge"></span>
</div>
</div>
<!-- Overview (read-only) -->
<div class="settings-section-card">
<h3>
<i class="fas fa-info-circle"></i>
Overview
</h3>
<div class="settings-grid">
<div class="settings-field">
<label>Key (provider ID)</label>
<code id="overview-key" class="value-mono"></code>
<small class="config-hint">
Provider-assigned URL-safe identifier. Read-only.
</small>
</div>
<div class="settings-field">
<label>Year</label>
<span id="overview-year" class="value"></span>
</div>
<div class="settings-field">
<label>Loading Status</label>
<span id="overview-loading-status" class="value"></span>
</div>
<div class="settings-field">
<label>Episode Count</label>
<span id="overview-episode-count" class="value"></span>
</div>
<div class="settings-field">
<label>Missing Episodes</label>
<span id="overview-missing-count" class="value"></span>
</div>
<div class="settings-field">
<label>NFO Created</label>
<span id="overview-nfo-created" class="value"></span>
</div>
<div class="settings-field">
<label>NFO Updated</label>
<span id="overview-nfo-updated" class="value"></span>
</div>
<div class="settings-field full-width">
<label>NFO Path</label>
<code id="overview-nfo-path" class="value-mono"></code>
</div>
</div>
</div>
<!-- Editable fields -->
<div class="settings-section-card">
<h3>
<i class="fas fa-pen-to-square"></i>
Editable Fields
</h3>
<p class="config-hint">
Changes are saved to the database. Use the action
buttons below to also rename the on-disk folder or
regenerate tvshow.nfo.
</p>
<div class="settings-grid">
<div class="settings-field">
<label for="field-name">Name</label>
<input type="text" id="field-name" class="input-field"
data-field="name" maxlength="500">
<small id="hint-name" class="config-hint"></small>
</div>
<div class="settings-field">
<label for="field-folder">Folder</label>
<input type="text" id="field-folder" class="input-field"
data-field="folder" maxlength="1000">
<small id="hint-folder" class="config-hint"></small>
</div>
<div class="settings-field">
<label for="field-tmdb-id">TMDB ID</label>
<input type="number" id="field-tmdb-id" class="input-field"
data-field="tmdb_id" min="1" max="9999999999" step="1">
<small id="hint-tmdb-id" class="config-hint">
Positive integer up to 10 digits.
</small>
</div>
<div class="settings-field">
<label for="field-tvdb-id">TVDB ID</label>
<input type="number" id="field-tvdb-id" class="input-field"
data-field="tvdb_id" min="1" max="9999999999" step="1">
<small id="hint-tvdb-id" class="config-hint">
Optional. Positive integer up to 10 digits.
</small>
</div>
<div class="settings-field full-width">
<label for="field-site">Site URL</label>
<input type="text" id="field-site" class="input-field"
data-field="site" maxlength="500">
<small id="hint-site" class="config-hint">
Provider URL (e.g. https://aniworld.to/anime/stream/...)
</small>
</div>
</div>
<div class="settings-actions">
<button id="save-db-btn" class="btn btn-primary">
<i class="fas fa-save"></i>
<span>Save to DB</span>
</button>
<button id="save-db-nfo-btn" class="btn btn-success">
<i class="fas fa-save"></i>
<i class="fas fa-file-lines"></i>
<span>Save &amp; Regenerate NFO</span>
</button>
<button id="reset-btn" class="btn btn-secondary">
<i class="fas fa-undo"></i>
<span>Reset</span>
</button>
</div>
<div class="settings-toggles">
<label class="checkbox-label">
<input type="checkbox" id="rename-disk-toggle">
<span class="checkbox-custom"></span>
<span>Also rename the folder on disk when folder changes</span>
</label>
</div>
</div>
<!-- NFO actions -->
<div class="settings-section-card">
<h3>
<i class="fas fa-file-lines"></i>
NFO Actions
</h3>
<p class="config-hint">
tvshow.nfo is consumed by Plex / Jellyfin / Emby /
Kodi. Use the buttons below to regenerate or view
its contents.
</p>
<div class="settings-actions">
<button id="regenerate-nfo-btn" class="btn btn-primary">
<i class="fas fa-rotate"></i>
<span>Regenerate tvshow.nfo</span>
</button>
<button id="view-nfo-btn" class="btn btn-secondary">
<i class="fas fa-eye"></i>
<span>View NFO XML</span>
</button>
</div>
<pre id="nfo-content" class="nfo-content hidden"></pre>
</div>
</section>
</main>
<!-- Toast notifications -->
<div id="toast-container" class="toast-container"></div>
</div>
<!-- Shared modules -->
<script src="/static/js/shared/constants.js?v={{ static_version('js/shared/constants.js') }}"></script>
<script src="/static/js/shared/auth.js?v={{ static_version('js/shared/auth.js') }}"></script>
<script src="/static/js/shared/api-client.js?v={{ static_version('js/shared/api-client.js') }}"></script>
<script src="/static/js/shared/ui-utils.js?v={{ static_version('js/shared/ui-utils.js') }}"></script>
<!-- Page script -->
<script src="/static/js/pages/anime-settings.js?v={{ static_version('js/pages/anime-settings.js') }}"></script>
</body>
</html>