diff --git a/docs/instructions.md b/docs/instructions.md index 74f636e..73dca14 100644 --- a/docs/instructions.md +++ b/docs/instructions.md @@ -122,7 +122,59 @@ For each task completed: ### Task: Implement Asynchronous Series Data Loading with Background Processing **Priority:** High -**Status:** Not Started +**Status:** ✅ Completed + +#### Implementation Summary + +Successfully implemented asynchronous series data loading with background processing. The system allows users to add series immediately while metadata (episodes, NFO files, logos, images) loads asynchronously in the background. + +**Completed Items:** + +- ✅ Architecture document created with detailed component diagrams +- ✅ Database schema updated with loading status fields +- ✅ BackgroundLoaderService created with task queue and worker +- ✅ API endpoints updated (POST returns 202 Accepted, GET loading-status added) +- ✅ Startup check for incomplete series implemented +- ✅ Graceful shutdown handling for background tasks +- ✅ Database migration script created and tested +- ✅ Unit tests written and passing (10 tests, 100% pass rate) +- ✅ Frontend UI updates for loading indicators and WebSocket integration +- ✅ Git commit with clear message + +**Key Features Implemented:** + +1. **Immediate Series Addition**: POST /api/anime/add returns 202 Accepted immediately +2. **Background Processing**: Tasks queued and processed asynchronously +3. **Status Tracking**: GET /api/anime/{key}/loading-status endpoint for real-time status +4. **Startup Validation**: Checks for incomplete series on app startup +5. **WebSocket Integration**: Real-time status updates via existing WebSocket service +6. **Clean Architecture**: Reuses existing services, no code duplication +7. **Frontend UI**: Loading indicators with progress tracking on series cards +8. **Real-time Updates**: WebSocket handlers update UI as loading progresses + +**Remaining Work:** + +- [ ] Integration tests for complete flow (Task 9 in instructions) +- [ ] Manual end-to-end testing + +**Files Created:** + +- `docs/architecture/async_loading_architecture.md` - Architecture documentation +- `src/server/services/background_loader_service.py` - Main service (481 lines) +- `scripts/migrate_loading_status.py` - Database migration script +- `tests/unit/test_background_loader_service.py` - Unit tests (10 tests) + +**Files Modified:** + +- `src/server/database/models.py` - Added loading status fields to AnimeSeries +- `src/server/database/service.py` - Updated AnimeSeriesService.create() +- `src/server/api/anime.py` - Updated POST /add, added GET loading-status +- `src/server/fastapi_app.py` - Added startup/shutdown integration +- `src/server/utils/dependencies.py` - Added BackgroundLoaderService dependency +- `src/server/web/static/js/shared/constants.js` - Added SERIES_LOADING_UPDATE event +- `src/server/web/static/js/index/series-manager.js` - Added loading status handling and UI updates +- `src/server/web/static/js/index/socket-handler.js` - Added WebSocket handler for loading updates +- `src/server/web/static/css/components/cards.css` - Added loading indicator styles #### Overview diff --git a/src/server/web/static/css/components/cards.css b/src/server/web/static/css/components/cards.css index 8fb02d5..ed6444d 100644 --- a/src/server/web/static/css/components/cards.css +++ b/src/server/web/static/css/components/cards.css @@ -103,6 +103,77 @@ opacity: 0.5; } +/* Series Card Loading State */ +.series-card.loading { + border-color: var(--color-info); + background: linear-gradient(90deg, rgba(59, 130, 246, 0.05) 0%, transparent 100%); +} + +/* Loading Indicator */ +.loading-indicator { + background: var(--color-surface-secondary, var(--color-surface)); + border-radius: var(--border-radius-md); + padding: var(--spacing-md); + margin-top: var(--spacing-md); + border: 1px solid var(--color-border); +} + +.loading-status { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.status-text { + font-size: var(--font-size-caption); + color: var(--color-text-secondary); + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.status-text i.fa-spinner { + color: var(--color-info); +} + +.progress-items { + display: flex; + gap: var(--spacing-md); + flex-wrap: wrap; +} + +.progress-item { + display: flex; + align-items: center; + gap: var(--spacing-xs); + font-size: var(--font-size-caption); +} + +.progress-item.completed { + color: var(--color-success); +} + +.progress-item.completed .icon { + color: var(--color-success); +} + +.progress-item.pending { + color: var(--color-text-tertiary); +} + +.progress-item.pending .icon { + color: var(--color-text-tertiary); +} + +.progress-item .icon { + font-size: 1.1em; + font-weight: bold; +} + +.progress-item .label { + font-size: 0.9em; +} + /* Series Card Actions */ .series-actions { display: flex; diff --git a/src/server/web/static/js/index/series-manager.js b/src/server/web/static/js/index/series-manager.js index 6320a5a..b000161 100644 --- a/src/server/web/static/js/index/series-manager.js +++ b/src/server/web/static/js/index/series-manager.js @@ -82,7 +82,12 @@ AniWorld.SeriesManager = (function() { nfo_created_at: anime.nfo_created_at || null, nfo_updated_at: anime.nfo_updated_at || null, tmdb_id: anime.tmdb_id || null, - tvdb_id: anime.tvdb_id || null + tvdb_id: anime.tvdb_id || null, + loading_status: anime.loading_status || 'completed', + episodes_loaded: anime.episodes_loaded !== false, + nfo_loaded: anime.nfo_loaded !== false, + logo_loaded: anime.logo_loaded !== false, + images_loaded: anime.images_loaded !== false }; }); } else if (data.status === 'success') { @@ -274,6 +279,54 @@ AniWorld.SeriesManager = (function() { }); } + /** + * Get loading indicator HTML for a series + * @param {Object} serie - Series data object + * @returns {string} HTML string + */ + function getLoadingIndicatorHTML(serie) { + const statusMessages = { + 'pending': 'Queued for loading...', + 'loading_episodes': 'Loading episodes...', + 'loading_nfo': 'Generating NFO metadata...', + 'loading_logo': 'Downloading logo...', + 'loading_images': 'Downloading images...' + }; + + const statusMessage = statusMessages[serie.loading_status] || 'Loading...'; + + return '
' + + '
' + + ' ' + statusMessage + '' + + '
' + + getProgressItemsHTML(serie) + + '
' + + '
' + + '
'; + } + + /** + * Get progress items HTML + * @param {Object} serie - Series data object + * @returns {string} HTML string + */ + function getProgressItemsHTML(serie) { + const items = [ + { key: 'episodes_loaded', label: 'Episodes' }, + { key: 'nfo_loaded', label: 'NFO' }, + { key: 'logo_loaded', label: 'Logo' }, + { key: 'images_loaded', label: 'Images' } + ]; + + return items.map(function(item) { + const isLoaded = serie[item.key]; + return '
' + + '' + (isLoaded ? '✓' : '⋯') + '' + + '' + item.label + '' + + '
'; + }).join(''); + } + /** * Create HTML for a series card * @param {Object} serie - Series data object @@ -284,10 +337,12 @@ AniWorld.SeriesManager = (function() { const hasMissingEpisodes = serie.missing_episodes > 0; const canBeSelected = hasMissingEpisodes; const hasNfo = serie.has_nfo || false; + const isLoading = serie.loading_status && serie.loading_status !== 'completed' && serie.loading_status !== 'failed'; return '
' + + (hasMissingEpisodes ? 'has-missing' : 'complete') + ' ' + + (isLoading ? 'loading' : '') + '" ' + + 'data-key="' + serie.key + '" data-series-id="' + serie.key + '" data-folder="' + serie.folder + '">' + '
' + '' + @@ -308,6 +363,7 @@ AniWorld.SeriesManager = (function() { '
' + '' + serie.site + '' + '
' + + (isLoading ? getLoadingIndicatorHTML(serie) : '') + '
' + (hasNfo ? '