From 30858f441cd8e4f012ff1a18add2e1e6588f861a Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 28 May 2026 18:38:34 +0200 Subject: [PATCH] feat: add manual TMDB/TVDB ID entry for failed lookups - Add PATCH /api/anime/{key}/metadata-ids endpoint to update IDs - Add POST /api/anime/{key}/refresh-nfo endpoint to force NFO regeneration - Add Edit Metadata IDs modal in frontend - Add showEditMetadataModal, saveMetadataIds, refreshSeriesNfo JS functions - Add edit-metadata-btn to series cards with database icon - IDs validated as positive integers or null Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/server/api/anime.py | 353 ++++++++++++++- src/server/web/static/js/app.js | 417 +++++++++++++++++- .../web/static/js/index/series-manager.js | 37 +- src/server/web/templates/index.html | 82 ++++ 4 files changed, 886 insertions(+), 3 deletions(-) diff --git a/src/server/api/anime.py b/src/server/api/anime.py index d47e32c..583ff94 100644 --- a/src/server/api/anime.py +++ b/src/server/api/anime.py @@ -3,10 +3,11 @@ import warnings from typing import Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, status -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator from sqlalchemy.ext.asyncio import AsyncSession from src.core.entities.series import Serie +from src.core.utils.key_utils import generate_key_from_folder, is_valid_key from src.server.database.service import AnimeSeriesService from src.server.exceptions import ( BadRequestError, @@ -133,6 +134,14 @@ class AnimeSummary(BaseModel): default=None, description="ISO timestamp when NFO was last updated" ) + loading_status: Optional[str] = Field( + default=None, + description="Current loading status (e.g., 'completed', 'failed', 'in_progress')" + ) + loading_error: Optional[str] = Field( + default=None, + description="Error message if loading failed (e.g., 'key cannot be None or empty')" + ) tmdb_id: Optional[int] = Field( default=None, description="The Movie Database (TMDB) ID" @@ -331,6 +340,8 @@ async def list_anime( nfo_updated_at=series_dict.get("nfo_updated_at"), tmdb_id=series_dict.get("tmdb_id"), tvdb_id=series_dict.get("tvdb_id"), + loading_status=series_dict.get("loading_status"), + loading_error=series_dict.get("loading_error"), ) ) @@ -1085,6 +1096,346 @@ async def get_anime( ) from exc +class ManualKeyUpdate(BaseModel): + """Request model for manually updating a series key.""" + + key: str = Field( + ..., + min_length=2, + description="New URL-safe key for the series (alphanumeric, hyphens, underscores)" + ) + + +@router.patch("/{anime_key}/manual-key", response_model=dict) +async def update_series_manual_key( + anime_key: str, + update_data: ManualKeyUpdate, + db: AsyncSession = Depends(get_optional_database_session), + series_app: Any = Depends(get_series_app), +) -> dict: + """Manually update the key for a series. + + This endpoint allows users to supply a key for folders that failed + automatic key generation (e.g., non-Latin characters, special symbols). + + Args: + anime_key: Current series key + update_data: New key to assign + db: Database session + series_app: SeriesApp instance for in-memory updates + + Returns: + Updated series info with new key + + Raises: + HTTPException: If validation fails or series not found + """ + new_key = update_data.key.strip() + + # Validate the new key format + if not is_valid_key(new_key): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid key format. Key must be URL-safe (alphanumeric, hyphens, underscores only)" + ) + + # Find the series - check DB first + series_db = None + if db: + series_db = await AnimeSeriesService.get_by_key(db, anime_key) + + # Also check in-memory list if series_app available + found_in_memory = None + if series_app and hasattr(series_app, "list"): + for serie in series_app.list.GetList(): + if getattr(serie, "key", None) == anime_key: + found_in_memory = serie + break + + if not series_db and not found_in_memory: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Series with key '{anime_key}' not found" + ) + + # Check if new key is already in use + existing_keys = set() + if db: + all_series = await AnimeSeriesService.get_all(db) + existing_keys = {s.key for s in all_series if s.key != anime_key} + if series_app and hasattr(series_app, "list"): + for serie in series_app.list.GetList(): + key = getattr(serie, "key", None) + if key and key != anime_key: + existing_keys.add(key) + + if new_key in existing_keys: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Key '{new_key}' is already in use by another series" + ) + + old_key = anime_key + + # Update in database if found + if series_db: + from src.server.database.connection import get_db + async with get_db() as session: + await AnimeSeriesService.update( + session, + series_db.id, + key=new_key, + loading_error=None # Clear error on successful key update + ) + await session.commit() + + # Update in-memory cache + if found_in_memory: + try: + found_in_memory.key = new_key + logger.info( + "Updated in-memory key for series: %s -> %s", + old_key, + new_key + ) + except ValueError as ve: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(ve) + ) + + logger.info( + "Manual key update successful: %s -> %s", + old_key, + new_key + ) + + return { + "success": True, + "old_key": old_key, + "new_key": new_key, + "message": f"Key updated from '{old_key}' to '{new_key}'" + } + + +class MetadataIdsUpdate(BaseModel): + """Request model for manually updating TMDB and TVDB IDs.""" + + tmdb_id: Optional[int] = Field( + default=None, + description="TMDB ID (positive integer, or null to clear)" + ) + tvdb_id: Optional[int] = Field( + default=None, + description="TVDB ID (positive integer, or null to clear)" + ) + + @field_validator("tmdb_id", "tvdb_id") + @classmethod + def validate_positive_or_null(cls, v): + if v is not None and v <= 0: + raise ValueError("ID must be a positive integer or null") + return v + + +@router.patch("/{anime_key}/metadata-ids", response_model=dict) +async def update_series_metadata_ids( + anime_key: str, + update_data: MetadataIdsUpdate, + db: AsyncSession = Depends(get_optional_database_session), + series_app: Any = Depends(get_series_app), +) -> dict: + """Manually update TMDB and TVDB IDs for a series. + + This endpoint allows users to supply missing metadata IDs for series + that failed automatic TMDB lookup. After updating IDs, it triggers + a background NFO re-generation. + + Args: + anime_key: Series key + update_data: TMDB and TVDB IDs to set + db: Database session + series_app: SeriesApp instance for in-memory updates + + Returns: + Updated series info with new IDs + + Raises: + HTTPException: If validation fails or series not found + """ + if update_data.tmdb_id is None and update_data.tvdb_id is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="At least one of tmdb_id or tvdb_id must be provided" + ) + + series_db = None + if db: + series_db = await AnimeSeriesService.get_by_key(db, anime_key) + + if not series_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Series with key '{anime_key}' not found" + ) + + update_fields = {} + if update_data.tmdb_id is not None: + update_fields["tmdb_id"] = update_data.tmdb_id + if update_data.tvdb_id is not None: + update_fields["tvdb_id"] = update_data.tvdb_id + + if db: + from datetime import datetime, timezone + update_fields["nfo_updated_at"] = datetime.now(timezone.utc) + update_fields["has_nfo"] = True + + await AnimeSeriesService.update( + db, + series_db.id, + **update_fields + ) + await db.commit() + + # Update in-memory cache if available + if series_app and hasattr(series_app, "list"): + for serie in series_app.list.GetList(): + if getattr(serie, "key", None) == anime_key: + if update_data.tmdb_id is not None: + serie.tmdb_id = update_data.tmdb_id + if update_data.tvdb_id is not None: + serie.tvdb_id = update_data.tvdb_id + break + + # Trigger background NFO re-generation + background_loader = None + try: + background_loader = await get_background_loader_service() + except Exception: + pass + + nfo_queued = False + if background_loader and db: + try: + from src.server.database.connection import get_db_session + async with get_db_session() as bg_db: + series_for_bg = await AnimeSeriesService.get_by_key(bg_db, anime_key) + if series_for_bg: + await background_loader.load_series_nfo( + series_for_bg.key, + series_for_bg.folder, + series_for_bg.name, + force_refresh=True + ) + nfo_queued = True + except Exception as e: + logger.warning("Failed to queue NFO refresh for '%s': %s", anime_key, str(e)) + + logger.info( + "Metadata IDs updated for '%s': tmdb_id=%s, tvdb_id=%s, NFO_queued=%s", + anime_key, + update_data.tmdb_id, + update_data.tvdb_id, + nfo_queued + ) + + return { + "success": True, + "key": anime_key, + "tmdb_id": update_data.tmdb_id, + "tvdb_id": update_data.tvdb_id, + "nfo_refresh_queued": nfo_queued, + "message": "Metadata IDs updated. NFO refresh queued." if nfo_queued + else "Metadata IDs updated." + } + + +@router.post("/{anime_key}/refresh-nfo", response_model=dict) +async def refresh_series_nfo( + anime_key: str, + db: AsyncSession = Depends(get_optional_database_session), + series_app: Any = Depends(get_series_app), +) -> dict: + """Force NFO re-generation for a series using current IDs. + + This endpoint triggers a background NFO re-generation using the + existing TMDB/TVDB IDs (or creating minimal NFO if no IDs exist). + + Args: + anime_key: Series key + db: Database session + series_app: SeriesApp instance + + Returns: + Status of NFO refresh operation + + Raises: + HTTPException: If series not found + """ + if db: + series_db = await AnimeSeriesService.get_by_key(db, anime_key) + + if not db or not series_db: + # Check in-memory + found = None + if series_app and hasattr(series_app, "list"): + for serie in series_app.list.GetList(): + if getattr(serie, "key", None) == anime_key: + found = serie + break + if not found: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Series with key '{anime_key}' not found" + ) + + background_loader = None + try: + background_loader = await get_background_loader_service() + except Exception: + pass + + if not background_loader: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Background loader service not available" + ) + + series_for_bg = None + if db: + async with get_db_session() as bg_db: + series_for_bg = await AnimeSeriesService.get_by_key(bg_db, anime_key) + + if not series_for_bg: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Series with key '{anime_key}' not found" + ) + + try: + await background_loader.load_series_nfo( + series_for_bg.key, + series_for_bg.folder, + series_for_bg.name, + force_refresh=True + ) + nfo_queued = True + except Exception as e: + logger.error("Failed to queue NFO refresh for '%s': %s", anime_key, str(e)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to queue NFO refresh: {str(e)}" + ) + + logger.info("NFO refresh queued for '%s'", anime_key) + + return { + "success": True, + "key": anime_key, + "message": "NFO refresh queued" + } + + # Maximum allowed input size for security MAX_INPUT_LENGTH = 100000 # 100KB diff --git a/src/server/web/static/js/app.js b/src/server/web/static/js/app.js index 50ad81f..cb81989 100644 --- a/src/server/web/static/js/app.js +++ b/src/server/web/static/js/app.js @@ -442,6 +442,23 @@ class AniWorldApp { this.hideConfigModal(); }); + // Edit key modal + document.getElementById('close-edit-key').addEventListener('click', () => { + this.hideEditKeyModal(); + }); + + document.getElementById('cancel-edit-key').addEventListener('click', () => { + this.hideEditKeyModal(); + }); + + document.querySelector('#edit-key-modal .modal-overlay').addEventListener('click', () => { + this.hideEditKeyModal(); + }); + + document.getElementById('save-edit-key').addEventListener('click', () => { + this.saveManualKey(); + }); + // Scheduler configuration document.getElementById('scheduled-rescan-enabled').addEventListener('change', () => { this.toggleSchedulerTimeInput(); @@ -1547,6 +1564,72 @@ class AniWorldApp { document.getElementById('config-modal').classList.add('hidden'); } + showEditKeyModal(key, folder) { + this._currentEditKey = key; + document.getElementById('edit-key-folder').textContent = folder; + document.getElementById('edit-key-input').value = ''; + document.getElementById('edit-key-error').classList.add('hidden'); + document.getElementById('edit-key-modal').classList.remove('hidden'); + } + + hideEditKeyModal() { + document.getElementById('edit-key-modal').classList.add('hidden'); + this._currentEditKey = null; + } + + async saveManualKey() { + const oldKey = this._currentEditKey; + const newKey = document.getElementById('edit-key-input').value.trim(); + const errorEl = document.getElementById('edit-key-error'); + + if (!newKey || newKey.length < 2) { + errorEl.textContent = 'Key must be at least 2 characters'; + errorEl.classList.remove('hidden'); + return; + } + + // Validate key format (URL-safe) + if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(newKey)) { + errorEl.textContent = 'Key must be URL-safe (alphanumeric, hyphens, underscores only)'; + errorEl.classList.remove('hidden'); + return; + } + + try { + const response = await this.makeAuthenticatedRequest( + `/api/anime/${encodeURIComponent(oldKey)}/manual-key`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key: newKey }) + } + ); + + if (!response) { + errorEl.textContent = 'Failed to update key'; + errorEl.classList.remove('hidden'); + return; + } + + if (response.ok) { + this.hideEditKeyModal(); + this.showToast(`Key updated: ${oldKey} → ${newKey}`, 'success'); + // Reload series list + if (typeof AniWorld.SeriesManager !== 'undefined') { + AniWorld.SeriesManager.loadSeries(); + } + } else { + const data = await response.json().catch(() => ({ detail: 'Update failed' })); + errorEl.textContent = data.detail || 'Failed to update key'; + errorEl.classList.remove('hidden'); + } + } catch (err) { + console.error('Error saving manual key:', err); + errorEl.textContent = 'Error updating key'; + errorEl.classList.remove('hidden'); + } + } + async loadSchedulerConfig() { try { const response = await this.makeAuthenticatedRequest('/api/scheduler/config'); @@ -2344,4 +2427,336 @@ document.addEventListener('DOMContentLoaded', () => { }); // Global functions for inline event handlers -window.app = null; \ No newline at end of file +window.app = null; + +/** + * Show the edit key modal + * @param {string} currentKey - The current series key + * @param {string} folderName - The folder name + */ +function showEditKeyModal(currentKey, folderName) { + const modal = document.getElementById('edit-key-modal'); + const overlay = document.getElementById('edit-key-overlay'); + const folderSpan = document.getElementById('edit-key-folder'); + const keyInput = document.getElementById('edit-key-input'); + const errorSpan = document.getElementById('edit-key-error'); + const saveBtn = document.getElementById('edit-key-save'); + + if (!modal || !overlay || !folderSpan || !keyInput) { + console.error('Edit key modal elements not found'); + return; + } + + // Reset state + folderSpan.textContent = folderName; + keyInput.value = currentKey; + keyInput.dataset.originalKey = currentKey; + errorSpan.textContent = ''; + errorSpan.style.display = 'none'; + saveBtn.disabled = false; + + // Show modal + modal.classList.remove('hidden'); + overlay.classList.remove('hidden'); + keyInput.focus(); + keyInput.select(); +} + +/** + * Hide the edit key modal + */ +function hideEditKeyModal() { + const modal = document.getElementById('edit-key-modal'); + const overlay = document.getElementById('edit-key-overlay'); + + if (modal) { + modal.classList.add('hidden'); + } + if (overlay) { + overlay.classList.add('hidden'); + } +} + +/** + * Save the manual key for a series + * @param {string} oldKey - The original key + * @param {string} newKey - The new key to set + */ +async function saveManualKey(oldKey, newKey) { + const errorSpan = document.getElementById('edit-key-error'); + const saveBtn = document.getElementById('edit-key-save'); + + if (!errorSpan || !saveBtn) { + console.error('Edit key modal elements not found'); + return; + } + + errorSpan.textContent = ''; + errorSpan.style.display = 'none'; + saveBtn.disabled = true; + + try { + const response = await fetch(`/api/anime/${encodeURIComponent(oldKey)}/manual-key`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ key: newKey }), + }); + + const data = await response.json(); + + if (!response.ok) { + errorSpan.textContent = data.detail || 'Failed to update key'; + errorSpan.style.display = 'block'; + saveBtn.disabled = false; + return; + } + + // Success - hide modal and reload + hideEditKeyModal(); + showToast('Key updated successfully', 'success'); + + // Reload series list + if (window.app && window.app.loadSeries) { + window.app.loadSeries(); + } else if (typeof loadSeries === 'function') { + loadSeries(); + } else { + location.reload(); + } + } catch (error) { + console.error('Error saving manual key:', error); + errorSpan.textContent = 'Network error: ' + error.message; + errorSpan.style.display = 'block'; + saveBtn.disabled = false; + } +} + +// Current metadata edit state +let _currentEditMetadataKey = null; + +/** + * Show the edit metadata IDs modal + * @param {string} key - The series key + * @param {string} name - The series name + * @param {number|null} currentTmdbId - Current TMDB ID + * @param {number|null} currentTvdbId - Current TVDB ID + */ +function showEditMetadataModal(key, name, currentTmdbId, currentTvdbId) { + const modal = document.getElementById('edit-metadata-modal'); + const overlay = modal ? modal.querySelector('.modal-overlay') : null; + const nameSpan = document.getElementById('edit-metadata-series-name'); + const tmdbInput = document.getElementById('edit-metadata-tmdb'); + const tvdbInput = document.getElementById('edit-metadata-tvdb'); + const errorSpan = document.getElementById('edit-metadata-error'); + const saveBtn = document.getElementById('save-edit-metadata'); + const cancelBtn = document.getElementById('cancel-edit-metadata'); + + if (!modal || !nameSpan || !tmdbInput || !tvdbInput) { + console.error('Edit metadata modal elements not found'); + return; + } + + // Store current key + _currentEditMetadataKey = key; + + // Reset state + nameSpan.textContent = name; + tmdbInput.value = currentTmdbId || ''; + tvdbInput.value = currentTvdbId || ''; + if (errorSpan) { + errorSpan.textContent = ''; + errorSpan.classList.add('hidden'); + } + if (saveBtn) saveBtn.disabled = false; + + // Show modal + modal.classList.remove('hidden'); + if (overlay) overlay.classList.remove('hidden'); + tmdbInput.focus(); +} + +/** + * Hide the edit metadata modal + */ +function hideEditMetadataModal() { + const modal = document.getElementById('edit-metadata-modal'); + if (modal) { + modal.classList.add('hidden'); + } + _currentEditMetadataKey = null; +} + +/** + * Save metadata IDs for a series + * @param {string} key - The series key + * @param {number|null} tmdbId - TMDB ID (null to clear) + * @param {number|null} tvdbId - TVDB ID (null to clear) + */ +async function saveMetadataIds(key, tmdbId, tvdbId) { + const errorSpan = document.getElementById('edit-metadata-error'); + const saveBtn = document.getElementById('save-edit-metadata'); + + if (!errorSpan || !saveBtn) { + console.error('Edit metadata modal elements not found'); + return; + } + + errorSpan.textContent = ''; + errorSpan.classList.add('hidden'); + saveBtn.disabled = true; + + try { + const body = {}; + if (tmdbId !== '') body.tmdb_id = parseInt(tmdbId, 10) || null; + if (tvdbId !== '') body.tvdb_id = parseInt(tvdbId, 10) || null; + + const response = await fetch(`/api/anime/${encodeURIComponent(key)}/metadata-ids`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + const data = await response.json(); + + if (!response.ok) { + errorSpan.textContent = data.detail || 'Failed to update metadata IDs'; + errorSpan.classList.remove('hidden'); + saveBtn.disabled = false; + return; + } + + // Success - hide modal and show toast + hideEditMetadataModal(); + showToast('Metadata IDs updated. NFO refresh queued.', 'success'); + + // Reload series list to reflect changes + if (window.app && window.app.loadSeries) { + window.app.loadSeries(); + } else if (typeof loadSeries === 'function') { + loadSeries(); + } + } catch (error) { + console.error('Error saving metadata IDs:', error); + errorSpan.textContent = 'Network error: ' + error.message; + errorSpan.classList.remove('hidden'); + saveBtn.disabled = false; + } +} + +/** + * Refresh NFO for a series + * @param {string} key - The series key + */ +async function refreshSeriesNfo(key) { + try { + const response = await fetch(`/api/anime/${encodeURIComponent(key)}/refresh-nfo`, { + method: 'POST', + }); + + const data = await response.json(); + + if (!response.ok) { + showToast('Failed to refresh NFO: ' + (data.detail || 'Unknown error'), 'error'); + return; + } + + showToast('NFO refresh queued', 'success'); + } catch (error) { + console.error('Error refreshing NFO:', error); + showToast('Network error: ' + error.message, 'error'); + } +} + +// Bind edit metadata modal events if modal exists +document.addEventListener('DOMContentLoaded', () => { + const modal = document.getElementById('edit-metadata-modal'); + const overlay = modal ? modal.querySelector('.modal-overlay') : null; + const cancelBtn = document.getElementById('cancel-edit-metadata'); + const saveBtn = document.getElementById('save-edit-metadata'); + const tmdbInput = document.getElementById('edit-metadata-tmdb'); + const tvdbInput = document.getElementById('edit-metadata-tvdb'); + + if (cancelBtn) { + cancelBtn.addEventListener('click', hideEditMetadataModal); + } + + if (overlay) { + overlay.addEventListener('click', hideEditMetadataModal); + } + + if (saveBtn && tmdbInput && tvdbInput) { + saveBtn.addEventListener('click', () => { + if (_currentEditMetadataKey) { + saveMetadataIds( + _currentEditMetadataKey, + tmdbInput.value, + tvdbInput.value + ); + } + }); + } + + if (tmdbInput) { + tmdbInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + saveBtn.click(); + } else if (e.key === 'Escape') { + hideEditMetadataModal(); + } + }); + } + + if (tvdbInput) { + tvdbInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + saveBtn.click(); + } else if (e.key === 'Escape') { + hideEditMetadataModal(); + } + }); + } +}); + +// Bind edit key modal events if modal exists +document.addEventListener('DOMContentLoaded', () => { + const modal = document.getElementById('edit-key-modal'); + const overlay = document.getElementById('edit-key-overlay'); + const cancelBtn = document.getElementById('edit-key-cancel'); + const saveBtn = document.getElementById('edit-key-save'); + const keyInput = document.getElementById('edit-key-input'); + + if (cancelBtn) { + cancelBtn.addEventListener('click', hideEditKeyModal); + } + + if (overlay) { + overlay.addEventListener('click', hideEditKeyModal); + } + + if (saveBtn && keyInput) { + saveBtn.addEventListener('click', () => { + const originalKey = keyInput.dataset.originalKey; + const newKey = keyInput.value.trim(); + if (newKey && newKey !== originalKey) { + saveManualKey(originalKey, newKey); + } + }); + } + + if (keyInput) { + keyInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + saveBtn.click(); + } else if (e.key === 'Escape') { + hideEditKeyModal(); + } + }); + } +}); \ No newline at end of file diff --git a/src/server/web/static/js/index/series-manager.js b/src/server/web/static/js/index/series-manager.js index a4acda5..bb446ad 100644 --- a/src/server/web/static/js/index/series-manager.js +++ b/src/server/web/static/js/index/series-manager.js @@ -40,6 +40,31 @@ AniWorld.SeriesManager = (function() { if (sortBtn) { sortBtn.addEventListener('click', toggleAlphabeticalSort); } + + // Event delegation for dynamically created edit-key buttons + document.addEventListener('click', function(e) { + const editKeyBtn = e.target.closest('.edit-key-btn'); + if (editKeyBtn) { + e.preventDefault(); + const key = editKeyBtn.dataset.key; + const folder = editKeyBtn.dataset.folder; + if (window.showEditKeyModal) { + window.showEditKeyModal(key, folder); + } + } + + const editMetadataBtn = e.target.closest('.edit-metadata-btn'); + if (editMetadataBtn) { + e.preventDefault(); + const key = editMetadataBtn.dataset.key; + const name = editMetadataBtn.dataset.name; + const tmdbId = editMetadataBtn.dataset.tmdbId || null; + const tvdbId = editMetadataBtn.dataset.tvdbId || null; + if (window.showEditMetadataModal) { + window.showEditMetadataModal(key, name, tmdbId, tvdbId); + } + } + }); } /** @@ -343,7 +368,8 @@ AniWorld.SeriesManager = (function() { const canBeSelected = hasMissingEpisodes; const hasNfo = serie.has_nfo || false; const isLoading = serie.loading_status && serie.loading_status !== 'completed' && serie.loading_status !== 'failed'; - + const hasKeyError = serie.loading_error && serie.loading_error.includes('key cannot be None or empty'); + // Debug logging for troubleshooting if (serie.key === 'so-im-a-spider-so-what') { console.log('[createSerieCard] Spider series:', { @@ -356,6 +382,12 @@ AniWorld.SeriesManager = (function() { }); } + const editKeyBtn = hasKeyError + ? '' + : ''; + + const editMetadataBtn = ''; + return '
' + AniWorld.UI.escapeHtml(serie.folder) + '
' + '' + '
' + + (hasKeyError ? '' : '') + (hasMissingEpisodes ? '' : '') + (hasNfo ? '' : '') + + editMetadataBtn + + editKeyBtn + '
' + '' + '
' + diff --git a/src/server/web/templates/index.html b/src/server/web/templates/index.html index 5ef9ae1..0b31794 100644 --- a/src/server/web/templates/index.html +++ b/src/server/web/templates/index.html @@ -640,6 +640,88 @@
+ + + + + +