Revert "feat: add manual TMDB/TVDB ID entry for failed lookups"

This reverts commit 30858f441c.
This commit is contained in:
2026-05-30 12:17:48 +02:00
parent 76b849fc91
commit 94ed013172
4 changed files with 4 additions and 885 deletions

View File

@@ -4,10 +4,11 @@ from pathlib import Path
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from src.config.settings import settings
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 (
@@ -232,14 +233,6 @@ 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"
@@ -438,8 +431,6 @@ 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"),
)
)
@@ -1194,346 +1185,6 @@ 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

View File

@@ -442,23 +442,6 @@ 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();
@@ -1564,72 +1547,6 @@ 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');
@@ -2427,336 +2344,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Global functions for inline event handlers
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();
}
});
}
});
window.app = null;

View File

@@ -40,31 +40,6 @@ 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);
}
}
});
}
/**
@@ -368,8 +343,7 @@ 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:', {
@@ -382,12 +356,6 @@ AniWorld.SeriesManager = (function() {
});
}
const editKeyBtn = hasKeyError
? '<button class="btn btn-icon edit-key-btn" title="Fix key error" data-key="' + serie.key + '" data-folder="' + serie.folder + '"><i class="fas fa-key"></i></button>'
: '';
const editMetadataBtn = '<button class="btn btn-icon edit-metadata-btn" title="Edit Metadata IDs" data-key="' + serie.key + '" data-name="' + AniWorld.UI.escapeHtml(serie.name) + '" data-tmdb-id="' + (serie.tmdb_id || '') + '" data-tvdb-id="' + (serie.tvdb_id || '') + '"><i class="fas fa-database"></i></button>';
return '<div class="series-card ' + (isSelected ? 'selected' : '') + ' ' +
(hasMissingEpisodes ? 'has-missing' : 'complete') + ' ' +
(isLoading ? 'loading' : '') + '" ' +
@@ -400,12 +368,9 @@ AniWorld.SeriesManager = (function() {
'<div class="series-folder">' + AniWorld.UI.escapeHtml(serie.folder) + '</div>' +
'</div>' +
'<div class="series-status">' +
(hasKeyError ? '<i class="fas fa-exclamation-triangle key-error-badge" title="Key error: ' + serie.loading_error + '"></i>' : '') +
(hasMissingEpisodes ? '' : '<i class="fas fa-check-circle status-complete" title="Complete"></i>') +
(hasNfo ? '<i class="fas fa-file-alt nfo-badge nfo-exists" title="NFO metadata available"></i>' :
'<i class="fas fa-file-alt nfo-badge nfo-missing" title="No NFO metadata"></i>') +
editMetadataBtn +
editKeyBtn +
'</div>' +
'</div>' +
'<div class="series-stats">' +

View File

@@ -640,88 +640,6 @@
</div>
</div>
<!-- Edit Key Modal -->
<div id="edit-key-modal" class="modal hidden">
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h3 data-text="edit-key-title">Edit Series Key</h3>
<button id="close-edit-key" class="btn btn-icon">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="config-item">
<label data-text="current-folder">Folder Name:</label>
<span id="edit-key-folder" class="config-value"></span>
</div>
<div class="config-item">
<label for="edit-key-input" data-text="new-key">New Key:</label>
<input type="text" id="edit-key-input" class="input-field"
placeholder="e.g., attack-on-titan" minlength="2">
<small class="config-hint" data-text="key-format-hint">
URL-safe key (alphanumeric, hyphens, underscores)
</small>
</div>
<div id="edit-key-error" class="config-error hidden"></div>
</div>
<div class="modal-footer">
<button id="save-edit-key" class="btn btn-primary">
<i class="fas fa-save"></i>
<span data-text="save">Save</span>
</button>
<button id="cancel-edit-key" class="btn btn-secondary">
<span data-text="cancel">Cancel</span>
</button>
</div>
</div>
</div>
<!-- Edit Metadata IDs Modal -->
<div id="edit-metadata-modal" class="modal hidden">
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h3 data-text="edit-metadata-title">Edit Metadata IDs</h3>
<button id="close-edit-metadata" class="btn btn-icon">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="config-item">
<label data-text="series-name">Series:</label>
<span id="edit-metadata-series-name" class="config-value"></span>
</div>
<div class="config-item">
<label for="edit-metadata-tmdb" data-text="tmdb-id">TMDB ID:</label>
<input type="number" id="edit-metadata-tmdb" class="input-field"
placeholder="e.g., 12345" min="1" step="1">
<small class="config-hint" data-text="tmdb-id-hint">
Leave blank to clear. Find IDs at themoviedb.org
</small>
</div>
<div class="config-item">
<label for="edit-metadata-tvdb" data-text="tvdb-id">TVDB ID:</label>
<input type="number" id="edit-metadata-tvdb" class="input-field"
placeholder="e.g., 67890" min="1" step="1">
<small class="config-hint" data-text="tvdb-id-hint">
Leave blank to clear. Find IDs at thetvdb.com
</small>
</div>
<div id="edit-metadata-error" class="config-error hidden"></div>
</div>
<div class="modal-footer">
<button id="save-edit-metadata" class="btn btn-primary">
<i class="fas fa-save"></i>
<span data-text="save-refresh">Save & Refresh NFO</span>
</button>
<button id="cancel-edit-metadata" class="btn btn-secondary">
<span data-text="cancel">Cancel</span>
</button>
</div>
</div>
</div>
<!-- Toast notifications -->
<div id="toast-container" class="toast-container"></div>
</div>