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>
This commit is contained in:
@@ -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;
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
? '<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' : '') + '" ' +
|
||||
@@ -368,9 +400,12 @@ 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">' +
|
||||
|
||||
Reference in New Issue
Block a user