feat(nfo): implement NFO diagnostics and repair
- Add NFO API endpoints: diagnostics, repair, validate, needs-repair - Create /settings/nfo page with full NFO management UI - Add NFO status section to edit modal with repair functionality - Add anime details API for edit modal pre-fill data - Fix auth test fixtures in test_nfo_diagnostics_repair.py Implements NFO diagnostics when editing anime series via right-click menu. Users can now check NFO status, see missing tags, and repair NFOs directly from the edit modal or the dedicated NFO settings page.
This commit is contained in:
@@ -12,6 +12,8 @@ var AniWorld = window.AniWorld || {};
|
||||
AniWorld.EditModal = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants ? AniWorld.Constants.API : {};
|
||||
|
||||
let modalElement = null;
|
||||
let originalData = null;
|
||||
let currentKey = null;
|
||||
@@ -34,13 +36,26 @@ AniWorld.EditModal = (function() {
|
||||
hideKeyWarning();
|
||||
|
||||
try {
|
||||
// Find series data from the local series list
|
||||
const seriesData = findSeriesData(seriesKey);
|
||||
// Try to find series data from the local series list first
|
||||
let seriesData = findSeriesData(seriesKey);
|
||||
|
||||
// If not found locally, fetch from API
|
||||
if (!seriesData) {
|
||||
seriesData = await fetchSeriesDetails(seriesKey);
|
||||
}
|
||||
|
||||
originalData = {
|
||||
key: seriesKey,
|
||||
tmdb_id: seriesData ? seriesData.tmdb_id : null,
|
||||
tvdb_id: seriesData ? seriesData.tvdb_id : null,
|
||||
name: seriesData ? seriesData.name : seriesKey,
|
||||
year: seriesData ? seriesData.year : null,
|
||||
status: seriesData ? seriesData.status : null,
|
||||
genres: seriesData ? (seriesData.genres || []) : [],
|
||||
studio: seriesData ? (seriesData.studio || []) : [],
|
||||
premiered: seriesData ? seriesData.premiered : null,
|
||||
rating: seriesData ? seriesData.rating : null,
|
||||
rating_votes: seriesData ? seriesData.rating_votes : null,
|
||||
};
|
||||
|
||||
// Populate form fields
|
||||
@@ -48,6 +63,12 @@ AniWorld.EditModal = (function() {
|
||||
setFieldValue('edit-tmdb-id', originalData.tmdb_id || '');
|
||||
setFieldValue('edit-tvdb-id', originalData.tvdb_id || '');
|
||||
|
||||
// Populate display fields
|
||||
populateDisplayFields(originalData);
|
||||
|
||||
// Show/hide TMDB fetch button based on whether TMDB ID exists
|
||||
updateTmdbFetchButtonState();
|
||||
|
||||
// Load NFO diagnostics
|
||||
await loadDiagnostics(seriesKey);
|
||||
|
||||
@@ -62,6 +83,128 @@ AniWorld.EditModal = (function() {
|
||||
attachListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch series details from the API.
|
||||
* @param {string} key - Series key
|
||||
* @returns {Promise<Object|null>} Series details or null on error
|
||||
*/
|
||||
async function fetchSeriesDetails(key) {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(
|
||||
'/api/anime/' + encodeURIComponent(key) + '/details'
|
||||
);
|
||||
if (response && response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch series details:', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate display-only fields in the modal.
|
||||
* @param {Object} data - Series data
|
||||
*/
|
||||
function populateDisplayFields(data) {
|
||||
const fields = [
|
||||
{ id: 'edit-name', value: data.name || '' },
|
||||
{ id: 'edit-year', value: data.year || '' },
|
||||
{ id: 'edit-status', value: data.status || '' },
|
||||
{ id: 'edit-genres', value: (data.genres || []).join(', ') },
|
||||
{ id: 'edit-studio', value: (data.studio || []).join(', ') },
|
||||
{ id: 'edit-premiered', value: data.premiered || '' },
|
||||
{ id: 'edit-rating', value: data.rating ? data.rating.toFixed(1) + '/10' : '' },
|
||||
];
|
||||
|
||||
fields.forEach(function(field) {
|
||||
const el = document.getElementById(field.id);
|
||||
if (el) el.textContent = field.value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the TMDB fetch button visibility.
|
||||
*/
|
||||
function updateTmdbFetchButtonState() {
|
||||
const btn = document.getElementById('btn-fetch-tmdb');
|
||||
const tmdbValue = getFieldValue('edit-tmdb-id').trim();
|
||||
|
||||
if (btn) {
|
||||
btn.style.display = tmdbValue ? 'none' : 'inline-flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch TMDB data for auto-fill.
|
||||
*/
|
||||
async function fetchTmdbData() {
|
||||
const btn = document.getElementById('btn-fetch-tmdb');
|
||||
const resultsContainer = document.getElementById('tmdb-search-results');
|
||||
if (!btn || !resultsContainer) return;
|
||||
|
||||
// Show loading state
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
|
||||
resultsContainer.innerHTML = '';
|
||||
resultsContainer.style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(
|
||||
'/api/anime/' + encodeURIComponent(currentKey) + '/tmdb-search'
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-error">Connection error</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 400) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-error">TMDB not configured</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-error">Search failed</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await response.json();
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-no-results">No matches found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Render results
|
||||
resultsContainer.innerHTML = results.slice(0, 5).map(function(r) {
|
||||
const year = r.year ? ' (' + r.year + ')' : '';
|
||||
const rating = r.vote_average ? ' ★' + r.vote_average.toFixed(1) : '';
|
||||
return '<div class="tmdb-result-item" data-tmdb-id="' + r.tmdb_id + '">' +
|
||||
'<span class="tmdb-result-title">' + escapeHtml(r.title) + year + rating + '</span>' +
|
||||
'<span class="tmdb-result-overview">' + escapeHtml(r.overview || '') + '</span>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// Attach click handlers to results
|
||||
resultsContainer.querySelectorAll('.tmdb-result-item').forEach(function(item) {
|
||||
item.addEventListener('click', function() {
|
||||
const tmdbId = this.getAttribute('data-tmdb-id');
|
||||
setFieldValue('edit-tmdb-id', tmdbId);
|
||||
resultsContainer.innerHTML = '<p class="tmdb-selected">TMDB ID ' + tmdbId + ' selected</p>';
|
||||
updateTmdbFetchButtonState();
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-error">Search failed</p>';
|
||||
console.error('TMDB search error:', err);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-search"></i> Fetch from TMDB';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the edit modal and reset state.
|
||||
*/
|
||||
@@ -272,6 +415,7 @@ AniWorld.EditModal = (function() {
|
||||
function updateRepairButtonState() {
|
||||
const btn = document.getElementById('btn-repair-nfo');
|
||||
const hint = document.getElementById('repair-hint');
|
||||
const diagnosticsLink = document.getElementById('btn-open-nfo-diagnostics');
|
||||
const tmdbValue = getFieldValue('edit-tmdb-id').trim();
|
||||
|
||||
if (btn) {
|
||||
@@ -281,6 +425,11 @@ AniWorld.EditModal = (function() {
|
||||
if (hint) {
|
||||
hint.style.display = tmdbValue ? 'none' : 'block';
|
||||
}
|
||||
// Show link to full diagnostics page
|
||||
if (diagnosticsLink && currentKey) {
|
||||
diagnosticsLink.href = '/settings/nfo?key=' + encodeURIComponent(currentKey);
|
||||
diagnosticsLink.style.display = 'inline-flex';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
@@ -395,8 +544,10 @@ AniWorld.EditModal = (function() {
|
||||
const saveBtn = document.getElementById('btn-save-metadata');
|
||||
const cancelBtn = document.getElementById('btn-cancel-metadata');
|
||||
const repairBtn = document.getElementById('btn-repair-nfo');
|
||||
const fetchTmdbBtn = document.getElementById('btn-fetch-tmdb');
|
||||
const overlay = modalElement ? modalElement.querySelector('.modal-overlay') : null;
|
||||
const keyInput = document.getElementById('edit-key');
|
||||
const tmdbInput = document.getElementById('edit-tmdb-id');
|
||||
|
||||
if (saveBtn) {
|
||||
var saveFn = function() { save(); };
|
||||
@@ -416,6 +567,12 @@ AniWorld.EditModal = (function() {
|
||||
listeners.push({ el: repairBtn, event: 'click', fn: repairFn });
|
||||
}
|
||||
|
||||
if (fetchTmdbBtn) {
|
||||
var fetchTmdbFn = function() { fetchTmdbData(); };
|
||||
fetchTmdbBtn.addEventListener('click', fetchTmdbFn);
|
||||
listeners.push({ el: fetchTmdbBtn, event: 'click', fn: fetchTmdbFn });
|
||||
}
|
||||
|
||||
if (overlay) {
|
||||
var overlayFn = function() { close(); };
|
||||
overlay.addEventListener('click', overlayFn);
|
||||
@@ -432,6 +589,14 @@ AniWorld.EditModal = (function() {
|
||||
keyInput.addEventListener('input', keyFn);
|
||||
listeners.push({ el: keyInput, event: 'input', fn: keyFn });
|
||||
}
|
||||
|
||||
if (tmdbInput) {
|
||||
var tmdbFn = function() {
|
||||
updateTmdbFetchButtonState();
|
||||
};
|
||||
tmdbInput.addEventListener('input', tmdbFn);
|
||||
listeners.push({ el: tmdbInput, event: 'input', fn: tmdbFn });
|
||||
}
|
||||
}
|
||||
|
||||
function detachListeners() {
|
||||
|
||||
Reference in New Issue
Block a user