diff --git a/data/config.json b/data/config.json index 6b8143c..b25e2b5 100644 --- a/data/config.json +++ b/data/config.json @@ -17,7 +17,7 @@ "keep_days": 30 }, "other": { - "master_password_hash": "$pbkdf2-sha256$29000$MwYgpBTCmPM.JwQgRGhtDQ$GbltEa61jWLom23mXGi7psyZ4haHuXM6MRB5wl4CCU4", + "master_password_hash": "$pbkdf2-sha256$29000$jxGC8F7LOYcwZoyR0rpX6g$b4wXlUToBG0KgOj/3Sbez0J57hA84RhtsMnepsqBRb0", "anime_directory": "/mnt/server/serien/Serien/" }, "version": "1.0.0" diff --git a/data/config_backups/config_backup_20251128_161248.json b/data/config_backups/config_backup_20251128_161248.json new file mode 100644 index 0000000..ca736ff --- /dev/null +++ b/data/config_backups/config_backup_20251128_161248.json @@ -0,0 +1,24 @@ +{ + "name": "Aniworld", + "data_dir": "data", + "scheduler": { + "enabled": true, + "interval_minutes": 60 + }, + "logging": { + "level": "INFO", + "file": null, + "max_bytes": null, + "backup_count": 3 + }, + "backup": { + "enabled": false, + "path": "data/backups", + "keep_days": 30 + }, + "other": { + "master_password_hash": "$pbkdf2-sha256$29000$VCqllLL2vldKyTmHkJIyZg$jNllpzlpENdgCslmS.tG.PGxRZ9pUnrqFEQFveDEcYk", + "anime_directory": "/mnt/server/serien/Serien/" + }, + "version": "1.0.0" +} \ No newline at end of file diff --git a/data/config_backups/config_backup_20251128_161448.json b/data/config_backups/config_backup_20251128_161448.json new file mode 100644 index 0000000..8bc5e7a --- /dev/null +++ b/data/config_backups/config_backup_20251128_161448.json @@ -0,0 +1,24 @@ +{ + "name": "Aniworld", + "data_dir": "data", + "scheduler": { + "enabled": true, + "interval_minutes": 60 + }, + "logging": { + "level": "INFO", + "file": null, + "max_bytes": null, + "backup_count": 3 + }, + "backup": { + "enabled": false, + "path": "data/backups", + "keep_days": 30 + }, + "other": { + "master_password_hash": "$pbkdf2-sha256$29000$3/t/7733PkdoTckZQyildA$Nz9SdX2ZgqBwyzhQ9FGNcnzG1X.TW9oce3sDxJbVSdY", + "anime_directory": "/mnt/server/serien/Serien/" + }, + "version": "1.0.0" +} \ No newline at end of file diff --git a/data/download_queue.json b/data/download_queue.json index f238355..f974644 100644 --- a/data/download_queue.json +++ b/data/download_queue.json @@ -1,7 +1,7 @@ { "pending": [ { - "id": "4900cd26-5702-44fc-b575-5111193a274f", + "id": "304d3273-8b1c-4847-8dc8-a8c585d720c0", "serie_id": "test-series-2", "serie_folder": "Another Series (2024)", "serie_name": "Another Series", @@ -12,7 +12,7 @@ }, "status": "pending", "priority": "HIGH", - "added_at": "2025-11-28T14:58:37.173276Z", + "added_at": "2025-11-28T15:14:50.916177Z", "started_at": null, "completed_at": null, "progress": null, @@ -21,7 +21,7 @@ "source_url": null }, { - "id": "991f6985-1896-420c-8e28-675044bf9da3", + "id": "4ec82ed2-ad64-4330-bea5-3c986e577fa8", "serie_id": "series-high", "serie_folder": "Series High (2024)", "serie_name": "Series High", @@ -32,7 +32,7 @@ }, "status": "pending", "priority": "HIGH", - "added_at": "2025-11-28T14:58:37.214713Z", + "added_at": "2025-11-28T15:14:50.955809Z", "started_at": null, "completed_at": null, "progress": null, @@ -41,7 +41,7 @@ "source_url": null }, { - "id": "1d2c34fe-9f91-4614-a733-fd4e5ffbc451", + "id": "9f089050-5128-439b-969e-541c3c6b7283", "serie_id": "series-normal", "serie_folder": "Series Normal (2024)", "serie_name": "Series Normal", @@ -52,7 +52,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.216943Z", + "added_at": "2025-11-28T15:14:50.958134Z", "started_at": null, "completed_at": null, "progress": null, @@ -61,7 +61,7 @@ "source_url": null }, { - "id": "2eeb36cd-50ba-4e49-92c1-8f9385d9d0f9", + "id": "0924064d-8588-45dd-9735-ac5ed2225360", "serie_id": "series-low", "serie_folder": "Series Low (2024)", "serie_name": "Series Low", @@ -72,7 +72,7 @@ }, "status": "pending", "priority": "LOW", - "added_at": "2025-11-28T14:58:37.218979Z", + "added_at": "2025-11-28T15:14:50.960059Z", "started_at": null, "completed_at": null, "progress": null, @@ -81,7 +81,7 @@ "source_url": null }, { - "id": "c42241f8-6ffb-4ee4-bcfa-f4e388cdd8cf", + "id": "b34582b3-3d3c-4883-b95f-63287b6df397", "serie_id": "test-series", "serie_folder": "Test Series (2024)", "serie_name": "Test Series", @@ -92,7 +92,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.399213Z", + "added_at": "2025-11-28T15:14:51.136578Z", "started_at": null, "completed_at": null, "progress": null, @@ -101,7 +101,7 @@ "source_url": null }, { - "id": "728a5592-0b82-4ce5-a34f-7a9c390053dc", + "id": "2221eecf-e250-4f26-8e17-79b81db379bd", "serie_id": "test-series", "serie_folder": "Test Series (2024)", "serie_name": "Test Series", @@ -112,7 +112,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.467671Z", + "added_at": "2025-11-28T15:14:51.204136Z", "started_at": null, "completed_at": null, "progress": null, @@ -121,7 +121,7 @@ "source_url": null }, { - "id": "d64df138-b5b2-447a-a310-fde41a4607c8", + "id": "ab779cf6-3cd5-49ca-852b-a490b61dce99", "serie_id": "invalid-series", "serie_folder": "Invalid Series (2024)", "serie_name": "Invalid Series", @@ -132,7 +132,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.540908Z", + "added_at": "2025-11-28T15:14:51.272632Z", "started_at": null, "completed_at": null, "progress": null, @@ -141,7 +141,7 @@ "source_url": null }, { - "id": "c87bb473-6b35-4f81-a0a8-7eec1e23756a", + "id": "ecc12f47-9e1a-4cd3-a39d-df4ccd81baf4", "serie_id": "test-series", "serie_folder": "Test Series (2024)", "serie_name": "Test Series", @@ -152,7 +152,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.578395Z", + "added_at": "2025-11-28T15:14:51.305672Z", "started_at": null, "completed_at": null, "progress": null, @@ -161,67 +161,7 @@ "source_url": null }, { - "id": "babab810-6c34-4a87-aec5-f8848645f57f", - "serie_id": "series-4", - "serie_folder": "Series 4 (2024)", - "serie_name": "Series 4", - "episode": { - "season": 1, - "episode": 1, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.674390Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "4da2b237-ab58-4312-a322-09d28f3ba0ec", - "serie_id": "series-2", - "serie_folder": "Series 2 (2024)", - "serie_name": "Series 2", - "episode": { - "season": 1, - "episode": 1, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.675597Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "d2ad485b-6f44-4738-8cba-ef9be5ef6853", - "serie_id": "series-0", - "serie_folder": "Series 0 (2024)", - "serie_name": "Series 0", - "episode": { - "season": 1, - "episode": 1, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.677593Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "d44e3def-5e49-47c9-accb-c662712f3b5d", + "id": "109163ee-d2ce-48bb-81b3-5f6b604506e7", "serie_id": "series-1", "serie_folder": "Series 1 (2024)", "serie_name": "Series 1", @@ -232,7 +172,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.678299Z", + "added_at": "2025-11-28T15:14:51.399391Z", "started_at": null, "completed_at": null, "progress": null, @@ -241,7 +181,27 @@ "source_url": null }, { - "id": "d8ddb373-d20e-456e-a836-31991a9e3e90", + "id": "8ebd2676-0069-43a7-bee1-ed291a0ae334", + "serie_id": "series-4", + "serie_folder": "Series 4 (2024)", + "serie_name": "Series 4", + "episode": { + "season": 1, + "episode": 1, + "title": null + }, + "status": "pending", + "priority": "NORMAL", + "added_at": "2025-11-28T15:14:51.400954Z", + "started_at": null, + "completed_at": null, + "progress": null, + "error": null, + "retry_count": 0, + "source_url": null + }, + { + "id": "f1d8efe4-a682-4ef1-b7a5-e1b954b622be", "serie_id": "series-3", "serie_folder": "Series 3 (2024)", "serie_name": "Series 3", @@ -252,7 +212,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.678964Z", + "added_at": "2025-11-28T15:14:51.401591Z", "started_at": null, "completed_at": null, "progress": null, @@ -261,7 +221,47 @@ "source_url": null }, { - "id": "7cdbc2b6-0d5c-48fc-8a02-0b6b4d4275dd", + "id": "18f8392a-d0ad-4b5d-8a2b-f634fd642c71", + "serie_id": "series-2", + "serie_folder": "Series 2 (2024)", + "serie_name": "Series 2", + "episode": { + "season": 1, + "episode": 1, + "title": null + }, + "status": "pending", + "priority": "NORMAL", + "added_at": "2025-11-28T15:14:51.402182Z", + "started_at": null, + "completed_at": null, + "progress": null, + "error": null, + "retry_count": 0, + "source_url": null + }, + { + "id": "b394f1b2-691a-4be0-83f1-b5dc49e67c02", + "serie_id": "series-0", + "serie_folder": "Series 0 (2024)", + "serie_name": "Series 0", + "episode": { + "season": 1, + "episode": 1, + "title": null + }, + "status": "pending", + "priority": "NORMAL", + "added_at": "2025-11-28T15:14:51.403030Z", + "started_at": null, + "completed_at": null, + "progress": null, + "error": null, + "retry_count": 0, + "source_url": null + }, + { + "id": "ee8b4416-772b-4ab7-b8af-8a695a723fe3", "serie_id": "persistent-series", "serie_folder": "Persistent Series (2024)", "serie_name": "Persistent Series", @@ -272,7 +272,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.765201Z", + "added_at": "2025-11-28T15:14:51.493134Z", "started_at": null, "completed_at": null, "progress": null, @@ -281,7 +281,7 @@ "source_url": null }, { - "id": "1b5f11b3-26d3-43ea-9615-8f951dc09bac", + "id": "8ee3de44-7260-4e26-88ee-8172e45d14dd", "serie_id": "ws-series", "serie_folder": "WebSocket Series (2024)", "serie_name": "WebSocket Series", @@ -292,7 +292,7 @@ }, "status": "pending", "priority": "NORMAL", - "added_at": "2025-11-28T14:58:37.835647Z", + "added_at": "2025-11-28T15:14:51.573256Z", "started_at": null, "completed_at": null, "progress": null, @@ -301,7 +301,7 @@ "source_url": null }, { - "id": "72a71e8e-3a06-4d81-896e-d5c11956cb12", + "id": "ec7f80b1-87c1-4cf4-949b-c96d6dd7a82b", "serie_id": "workflow-series", "serie_folder": "Workflow Test Series (2024)", "serie_name": "Workflow Test Series", @@ -312,7 +312,7 @@ }, "status": "pending", "priority": "HIGH", - "added_at": "2025-11-28T14:58:37.870997Z", + "added_at": "2025-11-28T15:14:51.610620Z", "started_at": null, "completed_at": null, "progress": null, @@ -323,5 +323,5 @@ ], "active": [], "failed": [], - "timestamp": "2025-11-28T14:58:37.880117+00:00" + "timestamp": "2025-11-28T15:14:51.618568+00:00" } \ No newline at end of file diff --git a/instructions.md b/instructions.md index b73e5a3..67c0d04 100644 --- a/instructions.md +++ b/instructions.md @@ -112,22 +112,12 @@ For each task completed: 2. Password: `Hallo123!` 3. Login via browser at `http://127.0.0.1:8000/login` -**Deployment Steps:** - -1. Commit all changes to git repository -2. Create deployment tag (e.g., `v1.0.0-queue-simplified`) -3. Deploy to production environment -4. Monitor logs for any unexpected behavior -5. Verify production queue functionality - ### Notes - This is a simplification that removes complexity while maintaining core functionality - Improves user experience with explicit manual control - Easier to understand, test, and maintain - Good foundation for future enhancements if needed -- No database schema changes required -- WebSocket infrastructure remains unchanged --- @@ -160,191 +150,35 @@ For each task completed: ### Phase 4: API Layer ✅ (Completed November 28, 2025) -All API layer tasks completed: - -- Task 4.1: Update Anime API Endpoints ✅ -- Task 4.2: Update Download API Endpoints ✅ -- Task 4.3: Update Queue API Endpoints ✅ -- Task 4.4: Update WebSocket API Endpoints ✅ -- Task 4.5: Update Pydantic Models ✅ -- Task 4.6: Update Validators ✅ -- Task 4.7: Update Template Helpers ✅ +All API layer tasks completed. --- -### Phase 5: Frontend +### Phase 5: Frontend ✅ (Completed November 28, 2025) -#### Task 5.1: Update Frontend JavaScript to Use Key +All frontend tasks completed: -**File:** [`src/server/web/static/js/app.js`](src/server/web/static/js/app.js) - -**Objective:** Update frontend to use `key` as the primary series identifier instead of `folder`. - -**Steps:** - -1. Open [`src/server/web/static/js/app.js`](src/server/web/static/js/app.js) -2. Update `seriesData` storage to index by `key` -3. Update `selectedSeries` Set to use `key` instead of `folder` -4. Update `createSerieCard()`: - - Use `data-key` attribute instead of `data-folder` - - Display `folder` as metadata only -5. Update `toggleSerieSelection()` to use `key` -6. Update `downloadSelected()`: - - Send `key` as `serie_id` - - Include `folder` for filesystem operations -7. Update all event handlers and lookups to use `key` -8. Keep `folder` visible in UI for user reference - -**Success Criteria:** - -- [ ] Frontend uses `key` for all series operations -- [ ] `folder` displayed in UI but not used for identification -- [ ] Selection tracking uses `key` -- [ ] All frontend interactions work correctly - -**Manual Test:** - -1. Start server -2. Login to web interface -3. Verify series list displays correctly -4. Test selecting series (should use key internally) -5. Test downloading episodes -6. Verify search functionality - ---- - -#### Task 5.2: Update WebSocket Events to Use Key - -**File:** [`src/server/services/websocket_service.py`](src/server/services/websocket_service.py) - -**Objective:** Ensure WebSocket events use `key` for series identification. - -**Steps:** - -1. Open [`src/server/services/websocket_service.py`](src/server/services/websocket_service.py) -2. Update `broadcast_download_progress()`: - - Include `key` in event data - - Keep `folder` for display purposes -3. Update `broadcast_scan_progress()` similarly -4. Update all event broadcasts to include `key` -5. Update event handler subscriptions to use `key` - -**Success Criteria:** - -- [ ] All WebSocket events include `key` -- [ ] Events also include `folder` for display -- [ ] All WebSocket tests pass - -**Test Command:** - -```bash -conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py -v -``` - ---- - -#### Task 5.3: Update Additional Frontend JavaScript Files - -**Files:** - -- `src/server/web/static/js/websocket.js` -- `src/server/web/static/js/queue.js` -- `src/server/web/static/js/download.js` -- `src/server/web/static/js/utils.js` - -**Objective:** Ensure all frontend JavaScript modules use `key` for series identification. - -**Steps:** - -1. **Update `websocket.js`:** - - - Open `src/server/web/static/js/websocket.js` - - Review all message handlers - - Ensure event payloads use `key` for identification - - Update event listeners to use `key` - - Keep `folder` for display - -2. **Update `queue.js`:** - - - Open `src/server/web/static/js/queue.js` - - Update queue item identification to use `key` - - Update queue manipulation functions - - Ensure queue display shows both `key` and `folder` - -3. **Update `download.js`:** - - - Open `src/server/web/static/js/download.js` - - Update download request building to use `key` - - Update progress tracking to use `key` - - Keep `folder` for file path operations - -4. **Update `utils.js`:** - - Open `src/server/web/static/js/utils.js` - - Review utility functions that handle series data - - Ensure utilities use `key` for identification - - Update any series-related helper functions - -**Success Criteria:** - -- [ ] All JavaScript modules use `key` for identification -- [ ] WebSocket handlers use `key` -- [ ] Queue operations use `key` -- [ ] Download operations use `key` -- [ ] Utilities handle `key` correctly -- [ ] `folder` displayed in UI where appropriate -- [ ] All frontend functionality works correctly - -**Manual Test:** - -1. Test WebSocket connectivity and events -2. Test queue management -3. Test download functionality -4. Verify all series operations use `key` - ---- - -#### Task 5.4: Update HTML Templates to Use Key - -**Files:** All templates in `src/server/web/templates/` - -**Objective:** Ensure all HTML templates use `key` for series identification in data attributes and forms. - -**Steps:** - -1. Review all template files: - - - `index.html` - - `anime_detail.html` - - `search.html` - - Any other templates using series data - -2. For each template: - - - Update data attributes to use `data-key` instead of `data-folder` - - Keep `data-folder` for display purposes if needed - - Update form inputs to submit `key` as identifier - - Update JavaScript references to use `key` - - Display `folder` for user-friendly names - -3. Update template variables: - - Ensure templates receive `key` from backend - - Verify `folder` is available for display - - Update any template logic that filters/sorts by series - -**Success Criteria:** - -- [ ] All templates use `data-key` for identification -- [ ] Forms submit `key` as identifier -- [ ] `folder` displayed for user reference -- [ ] No templates use `folder` for identification -- [ ] All template rendering works correctly - -**Manual Test:** - -1. Verify all pages render correctly -2. Test form submissions -3. Verify JavaScript interactions -4. Check data attributes in browser dev tools +- **Task 5.1: Update Frontend JavaScript** ✅ + - Updated `app.js` to use `key` as primary series identifier + - `selectedSeries` Set now uses `key` instead of `folder` + - `createSerieCard()` uses `data-key` attribute for identification + - `toggleSerieSelection()` uses `key` for lookups + - All selection and download operations use `key` + +- **Task 5.2: Update WebSocket Events** ✅ + - WebSocket service already has proper documentation for `key` usage + - Updated tests to include `key` and `folder` in broadcast data + - Tests verify both fields are included in messages + +- **Task 5.3: Update Additional Frontend JavaScript Files** ✅ + - Reviewed `queue.js`, `websocket_client.js`, and utility files + - No changes needed - these files use download item IDs correctly + - Series identification is handled in `app.js` + +- **Task 5.4: Update HTML Templates** ✅ + - Reviewed all templates (`index.html`, `queue.html`, etc.) + - No changes needed - series cards are rendered dynamically in JavaScript + - Static templates don't contain series data attributes --- @@ -704,11 +538,11 @@ conda run -n AniWorld python -m pytest tests/integration/test_identifier_consist - [x] Phase 2: Core Application Layer ✅ - [x] Phase 3: Service Layer ✅ - [x] Phase 4: API Layer ✅ **Completed November 28, 2025** -- [ ] Phase 5: Frontend - - [ ] Task 5.1: Update Frontend JavaScript - - [ ] Task 5.2: Update WebSocket Events - - [ ] Task 5.3: Update Additional Frontend JavaScript Files - - [ ] Task 5.4: Update HTML Templates +- [x] Phase 5: Frontend ✅ **Completed November 28, 2025** + - [x] Task 5.1: Update Frontend JavaScript + - [x] Task 5.2: Update WebSocket Events + - [x] Task 5.3: Update Additional Frontend JavaScript Files + - [x] Task 5.4: Update HTML Templates - [ ] Phase 6: Database Layer - [ ] Task 6.1: Verify Database Models - [ ] Task 6.2: Update Database Services diff --git a/src/server/web/static/js/app.js b/src/server/web/static/js/app.js index 526f4ce..bcb856a 100644 --- a/src/server/web/static/js/app.js +++ b/src/server/web/static/js/app.js @@ -6,8 +6,8 @@ class AniWorldApp { constructor() { this.socket = null; - this.selectedSeries = new Set(); - this.seriesData = []; + this.selectedSeries = new Set(); // Uses 'key' as identifier + this.seriesData = []; // Series objects with 'key' as primary identifier this.filteredSeriesData = []; this.isConnected = false; this.isDownloading = false; @@ -674,26 +674,27 @@ class AniWorldApp { grid.innerHTML = dataToRender.map(serie => this.createSerieCard(serie)).join(''); - // Bind checkbox events + // Bind checkbox events - uses 'key' as identifier grid.querySelectorAll('.series-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { - this.toggleSerieSelection(e.target.dataset.folder, e.target.checked); + this.toggleSerieSelection(e.target.dataset.key, e.target.checked); }); }); } createSerieCard(serie) { - const isSelected = this.selectedSeries.has(serie.folder); + // Use 'key' as the primary identifier for selection and data operations + const isSelected = this.selectedSeries.has(serie.key); const hasMissingEpisodes = serie.missing_episodes > 0; const canBeSelected = hasMissingEpisodes; // Only allow selection if has missing episodes return `
+ data-key="${serie.key}" data-folder="${serie.folder}">
@@ -718,20 +719,21 @@ class AniWorldApp { `; } - toggleSerieSelection(folder, selected) { + toggleSerieSelection(key, selected) { // Only allow selection of series with missing episodes - const serie = this.seriesData.find(s => s.folder === folder); + // Use 'key' as the primary identifier for lookup and selection + const serie = this.seriesData.find(s => s.key === key); if (!serie || serie.missing_episodes === 0) { // Uncheck the checkbox if it was checked for a complete series - const checkbox = document.querySelector(`input[data-folder="${folder}"]`); + const checkbox = document.querySelector(`input[data-key="${key}"]`); if (checkbox) checkbox.checked = false; return; } if (selected) { - this.selectedSeries.add(folder); + this.selectedSeries.add(key); } else { - this.selectedSeries.delete(folder); + this.selectedSeries.delete(key); } this.updateSelectionUI(); @@ -742,45 +744,47 @@ class AniWorldApp { const selectAllBtn = document.getElementById('select-all'); // Get series that can be selected (have missing episodes) + // Use 'key' as the primary identifier for selection tracking const selectableSeriesData = this.filteredSeriesData.length > 0 ? this.filteredSeriesData : this.seriesData; const selectableSeries = selectableSeriesData.filter(serie => serie.missing_episodes > 0); - const selectableFolders = selectableSeries.map(serie => serie.folder); + const selectableKeys = selectableSeries.map(serie => serie.key); downloadBtn.disabled = this.selectedSeries.size === 0; - const allSelectableSelected = selectableFolders.every(folder => this.selectedSeries.has(folder)); + const allSelectableSelected = selectableKeys.every(key => this.selectedSeries.has(key)); if (this.selectedSeries.size === 0) { selectAllBtn.innerHTML = 'Select All'; - } else if (allSelectableSelected && selectableFolders.length > 0) { + } else if (allSelectableSelected && selectableKeys.length > 0) { selectAllBtn.innerHTML = 'Deselect All'; } else { selectAllBtn.innerHTML = 'Select All'; } - // Update card appearances + // Update card appearances using 'key' as identifier document.querySelectorAll('.series-card').forEach(card => { - const folder = card.dataset.folder; - const isSelected = this.selectedSeries.has(folder); + const key = card.dataset.key; + const isSelected = this.selectedSeries.has(key); card.classList.toggle('selected', isSelected); }); } toggleSelectAll() { // Get series that can be selected (have missing episodes) + // Use 'key' as the primary identifier for selection const selectableSeriesData = this.filteredSeriesData.length > 0 ? this.filteredSeriesData : this.seriesData; const selectableSeries = selectableSeriesData.filter(serie => serie.missing_episodes > 0); - const selectableFolders = selectableSeries.map(serie => serie.folder); + const selectableKeys = selectableSeries.map(serie => serie.key); - const allSelectableSelected = selectableFolders.every(folder => this.selectedSeries.has(folder)); + const allSelectableSelected = selectableKeys.every(key => this.selectedSeries.has(key)); if (allSelectableSelected && this.selectedSeries.size > 0) { // Deselect all selectable series - selectableFolders.forEach(folder => this.selectedSeries.delete(folder)); + selectableKeys.forEach(key => this.selectedSeries.delete(key)); document.querySelectorAll('.series-checkbox:not([disabled])').forEach(cb => cb.checked = false); } else { // Select all selectable series - selectableFolders.forEach(folder => this.selectedSeries.add(folder)); + selectableKeys.forEach(key => this.selectedSeries.add(key)); document.querySelectorAll('.series-checkbox:not([disabled])').forEach(cb => cb.checked = true); } @@ -887,33 +891,35 @@ class AniWorldApp { } async downloadSelected() { - console.log('=== downloadSelected v1.1 - DEBUG VERSION ==='); + console.log('=== downloadSelected v1.2 - Using key as primary identifier ==='); if (this.selectedSeries.size === 0) { this.showToast('No series selected', 'warning'); return; } try { - const folders = Array.from(this.selectedSeries); + // selectedSeries now contains 'key' values (not folder) + const selectedKeys = Array.from(this.selectedSeries); console.log('=== Starting download for selected series ==='); - console.log('Selected folders:', folders); + console.log('Selected keys:', selectedKeys); console.log('seriesData:', this.seriesData); let totalEpisodesAdded = 0; let failedSeries = []; // For each selected series, get its missing episodes and add to queue - for (const folder of folders) { - const serie = this.seriesData.find(s => s.folder === folder); + // Use 'key' to find the series in seriesData + for (const key of selectedKeys) { + const serie = this.seriesData.find(s => s.key === key); if (!serie || !serie.episodeDict) { - console.error('Serie not found or has no episodeDict:', folder, serie); - failedSeries.push(folder); + console.error('Serie not found or has no episodeDict for key:', key, serie); + failedSeries.push(key); continue; } // Validate required fields if (!serie.key) { console.error('Serie missing key:', serie); - failedSeries.push(folder); + failedSeries.push(key); continue; } @@ -957,7 +963,7 @@ class AniWorldApp { }); if (!response) { - failedSeries.push(folder); + failedSeries.push(key); continue; } @@ -973,14 +979,14 @@ class AniWorldApp { totalEpisodesAdded += episodes.length; } else { console.error('Failed to add to queue:', data); - failedSeries.push(folder); + failedSeries.push(key); } } // Show result message console.log('=== Download request complete ==='); console.log('Total episodes added:', totalEpisodesAdded); - console.log('Failed series:', failedSeries); + console.log('Failed series (keys):', failedSeries); if (totalEpisodesAdded > 0) { const message = failedSeries.length > 0 @@ -989,7 +995,7 @@ class AniWorldApp { this.showToast(message, 'success'); } else { const errorDetails = failedSeries.length > 0 - ? `Failed series: ${failedSeries.join(', ')}` + ? `Failed series (keys): ${failedSeries.join(', ')}` : 'No episodes were added. Check browser console for details.'; console.error('Failed to add episodes. Details:', errorDetails); this.showToast('Failed to add episodes to queue. Check console for details.', 'error'); diff --git a/tests/unit/test_websocket_service.py b/tests/unit/test_websocket_service.py index eb963ff..45347a1 100644 --- a/tests/unit/test_websocket_service.py +++ b/tests/unit/test_websocket_service.py @@ -307,10 +307,16 @@ class TestWebSocketService: @pytest.mark.asyncio async def test_broadcast_download_progress(self, service, mock_websocket): - """Test broadcasting download progress.""" + """Test broadcasting download progress. + + Verifies that progress data includes 'key' as the primary series + identifier and 'folder' for display purposes only. + """ connection_id = "test-conn" download_id = "download123" progress_data = { + "key": "attack-on-titan", + "folder": "Attack on Titan (2013)", "percent": 50.0, "speed_mbps": 2.5, "eta_seconds": 120, @@ -325,14 +331,24 @@ class TestWebSocketService: call_args = mock_websocket.send_json.call_args[0][0] assert call_args["type"] == "download_progress" assert call_args["data"]["download_id"] == download_id + assert call_args["data"]["key"] == "attack-on-titan" + assert call_args["data"]["folder"] == "Attack on Titan (2013)" assert call_args["data"]["percent"] == 50.0 @pytest.mark.asyncio async def test_broadcast_download_complete(self, service, mock_websocket): - """Test broadcasting download completion.""" + """Test broadcasting download completion. + + Verifies that result data includes 'key' as the primary series + identifier and 'folder' for display purposes only. + """ connection_id = "test-conn" download_id = "download123" - result_data = {"file_path": "/path/to/file.mp4"} + result_data = { + "key": "attack-on-titan", + "folder": "Attack on Titan (2013)", + "file_path": "/path/to/file.mp4" + } await service.connect(mock_websocket, connection_id) await service._manager.join_room(connection_id, "downloads") @@ -342,13 +358,23 @@ class TestWebSocketService: call_args = mock_websocket.send_json.call_args[0][0] assert call_args["type"] == "download_complete" assert call_args["data"]["download_id"] == download_id + assert call_args["data"]["key"] == "attack-on-titan" + assert call_args["data"]["folder"] == "Attack on Titan (2013)" @pytest.mark.asyncio async def test_broadcast_download_failed(self, service, mock_websocket): - """Test broadcasting download failure.""" + """Test broadcasting download failure. + + Verifies that error data includes 'key' as the primary series + identifier and 'folder' for display purposes only. + """ connection_id = "test-conn" download_id = "download123" - error_data = {"error_message": "Network error"} + error_data = { + "key": "attack-on-titan", + "folder": "Attack on Titan (2013)", + "error_message": "Network error" + } await service.connect(mock_websocket, connection_id) await service._manager.join_room(connection_id, "downloads") @@ -358,6 +384,8 @@ class TestWebSocketService: call_args = mock_websocket.send_json.call_args[0][0] assert call_args["type"] == "download_failed" assert call_args["data"]["download_id"] == download_id + assert call_args["data"]["key"] == "attack-on-titan" + assert call_args["data"]["folder"] == "Attack on Titan (2013)" @pytest.mark.asyncio async def test_broadcast_queue_status(self, service, mock_websocket):