feat: Add NFO UI features (Task 6)

- Extended AnimeSummary model with NFO fields (has_nfo, nfo_created_at, nfo_updated_at, tmdb_id, tvdb_id)
- Updated list_anime endpoint to fetch and return NFO data from database
- Added NFO status badges to series cards (green=exists, gray=missing)
- Created nfo-manager.js module with createNFO, refreshNFO, viewNFO operations
- Added NFO action buttons to series cards (Create/View/Refresh)
- Integrated WebSocket handlers for real-time NFO events (creating, completed, failed)
- Added CSS styles for NFO badges and action buttons
- All 34 NFO API tests passing, all 32 anime endpoint tests passing
- Documented in docs/task6_status.md (90% complete, NFO status page deferred)
This commit is contained in:
2026-01-16 19:18:50 +01:00
parent d642234814
commit ecfa8d3c10
9 changed files with 699 additions and 5 deletions

View File

@@ -0,0 +1,239 @@
/**
* NFO Manager Module
*
* Handles NFO metadata operations including creating, viewing, and refreshing
* NFO files for anime series.
*/
window.AniWorld = window.AniWorld || {};
AniWorld.NFOManager = (function() {
'use strict';
/**
* Create NFO metadata for a series
* @param {string} seriesKey - The unique identifier for the series
* @returns {Promise<object>} API response
*/
async function createNFO(seriesKey) {
try {
AniWorld.UI.showLoading('Creating NFO metadata...');
const response = await AniWorld.ApiClient.request(
`/api/nfo/series/${encodeURIComponent(seriesKey)}`,
{
method: 'POST'
}
);
if (response && response.status === 'success') {
AniWorld.UI.showToast('NFO creation started', 'success');
return response;
} else {
throw new Error(response?.message || 'Failed to create NFO');
}
} catch (error) {
console.error('Error creating NFO:', error);
AniWorld.UI.showToast(
'Failed to create NFO: ' + error.message,
'error'
);
throw error;
} finally {
AniWorld.UI.hideLoading();
}
}
/**
* Refresh NFO metadata for a series (update existing NFO)
* @param {string} seriesKey - The unique identifier for the series
* @returns {Promise<object>} API response
*/
async function refreshNFO(seriesKey) {
try {
AniWorld.UI.showLoading('Refreshing NFO metadata...');
const response = await AniWorld.ApiClient.request(
`/api/nfo/series/${encodeURIComponent(seriesKey)}`,
{
method: 'PUT'
}
);
if (response && response.status === 'success') {
AniWorld.UI.showToast('NFO refresh started', 'success');
return response;
} else {
throw new Error(response?.message || 'Failed to refresh NFO');
}
} catch (error) {
console.error('Error refreshing NFO:', error);
AniWorld.UI.showToast(
'Failed to refresh NFO: ' + error.message,
'error'
);
throw error;
} finally {
AniWorld.UI.hideLoading();
}
}
/**
* View NFO metadata for a series
* @param {string} seriesKey - The unique identifier for the series
* @returns {Promise<object>} NFO data
*/
async function viewNFO(seriesKey) {
try {
AniWorld.UI.showLoading('Loading NFO data...');
const response = await AniWorld.ApiClient.request(
`/api/nfo/series/${encodeURIComponent(seriesKey)}`
);
if (response && response.data) {
return response.data;
} else {
throw new Error('No NFO data available');
}
} catch (error) {
console.error('Error viewing NFO:', error);
AniWorld.UI.showToast(
'Failed to load NFO: ' + error.message,
'error'
);
throw error;
} finally {
AniWorld.UI.hideLoading();
}
}
/**
* Show NFO data in a modal
* @param {string} seriesKey - The unique identifier for the series
*/
async function showNFOModal(seriesKey) {
try {
const nfoData = await viewNFO(seriesKey);
// Format NFO data for display
const nfoHtml = formatNFOData(nfoData);
// Show modal (assuming a modal utility exists)
if (AniWorld.UI.showModal) {
AniWorld.UI.showModal({
title: 'NFO Metadata',
content: nfoHtml,
size: 'large'
});
} else {
// Fallback: log to console
console.log('NFO Data:', nfoData);
alert('NFO Data:\n' + JSON.stringify(nfoData, null, 2));
}
} catch (error) {
console.error('Error showing NFO modal:', error);
}
}
/**
* Format NFO data for display in HTML
* @param {object} nfoData - NFO metadata object
* @returns {string} HTML string
*/
function formatNFOData(nfoData) {
let html = '<div class="nfo-data">';
if (nfoData.title) {
html += '<div class="nfo-field"><strong>Title:</strong> ' +
AniWorld.UI.escapeHtml(nfoData.title) + '</div>';
}
if (nfoData.plot) {
html += '<div class="nfo-field"><strong>Plot:</strong> ' +
AniWorld.UI.escapeHtml(nfoData.plot) + '</div>';
}
if (nfoData.year) {
html += '<div class="nfo-field"><strong>Year:</strong> ' +
nfoData.year + '</div>';
}
if (nfoData.genre) {
const genres = Array.isArray(nfoData.genre)
? nfoData.genre.join(', ')
: nfoData.genre;
html += '<div class="nfo-field"><strong>Genre:</strong> ' +
AniWorld.UI.escapeHtml(genres) + '</div>';
}
if (nfoData.rating) {
html += '<div class="nfo-field"><strong>Rating:</strong> ' +
nfoData.rating + '</div>';
}
if (nfoData.tmdb_id) {
html += '<div class="nfo-field"><strong>TMDB ID:</strong> ' +
nfoData.tmdb_id + '</div>';
}
if (nfoData.tvdb_id) {
html += '<div class="nfo-field"><strong>TVDB ID:</strong> ' +
nfoData.tvdb_id + '</div>';
}
html += '</div>';
return html;
}
/**
* Get NFO statistics
* @returns {Promise<object>} Statistics data
*/
async function getStatistics() {
try {
const response = await AniWorld.ApiClient.request('/api/nfo/statistics');
if (response && response.data) {
return response.data;
} else {
throw new Error('Failed to get NFO statistics');
}
} catch (error) {
console.error('Error getting NFO statistics:', error);
throw error;
}
}
/**
* Get series without NFO
* @param {number} limit - Maximum number of results
* @returns {Promise<Array>} List of series without NFO
*/
async function getSeriesWithoutNFO(limit = 10) {
try {
const response = await AniWorld.ApiClient.request(
`/api/nfo/missing?limit=${limit}`
);
if (response && response.data) {
return response.data;
} else {
throw new Error('Failed to get series without NFO');
}
} catch (error) {
console.error('Error getting series without NFO:', error);
throw error;
}
}
// Public API
return {
createNFO: createNFO,
refreshNFO: refreshNFO,
viewNFO: viewNFO,
showNFOModal: showNFOModal,
getStatistics: getStatistics,
getSeriesWithoutNFO: getSeriesWithoutNFO
};
})();