Phase 5: Frontend - Use key as primary series identifier

- 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
  - downloadSelected() iterates with key values
  - updateSelectionUI() and toggleSelectAll() use key

- Updated WebSocket service tests
  - Tests now include key and folder in broadcast data
  - Verified both fields are included in messages

- No changes needed for queue.js and other JS files
  - They use download item IDs correctly, not series identifiers

- No template changes needed
  - Series cards rendered dynamically in app.js

All 996 tests passing
This commit is contained in:
Lukas 2025-11-28 16:18:33 +01:00
parent 5aabad4d13
commit a833077f97
7 changed files with 238 additions and 322 deletions

View File

@ -17,7 +17,7 @@
"keep_days": 30 "keep_days": 30
}, },
"other": { "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/" "anime_directory": "/mnt/server/serien/Serien/"
}, },
"version": "1.0.0" "version": "1.0.0"

View File

@ -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"
}

View File

@ -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"
}

View File

@ -1,7 +1,7 @@
{ {
"pending": [ "pending": [
{ {
"id": "4900cd26-5702-44fc-b575-5111193a274f", "id": "304d3273-8b1c-4847-8dc8-a8c585d720c0",
"serie_id": "test-series-2", "serie_id": "test-series-2",
"serie_folder": "Another Series (2024)", "serie_folder": "Another Series (2024)",
"serie_name": "Another Series", "serie_name": "Another Series",
@ -12,7 +12,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "HIGH", "priority": "HIGH",
"added_at": "2025-11-28T14:58:37.173276Z", "added_at": "2025-11-28T15:14:50.916177Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -21,7 +21,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "991f6985-1896-420c-8e28-675044bf9da3", "id": "4ec82ed2-ad64-4330-bea5-3c986e577fa8",
"serie_id": "series-high", "serie_id": "series-high",
"serie_folder": "Series High (2024)", "serie_folder": "Series High (2024)",
"serie_name": "Series High", "serie_name": "Series High",
@ -32,7 +32,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "HIGH", "priority": "HIGH",
"added_at": "2025-11-28T14:58:37.214713Z", "added_at": "2025-11-28T15:14:50.955809Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -41,7 +41,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "1d2c34fe-9f91-4614-a733-fd4e5ffbc451", "id": "9f089050-5128-439b-969e-541c3c6b7283",
"serie_id": "series-normal", "serie_id": "series-normal",
"serie_folder": "Series Normal (2024)", "serie_folder": "Series Normal (2024)",
"serie_name": "Series Normal", "serie_name": "Series Normal",
@ -52,7 +52,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.216943Z", "added_at": "2025-11-28T15:14:50.958134Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -61,7 +61,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "2eeb36cd-50ba-4e49-92c1-8f9385d9d0f9", "id": "0924064d-8588-45dd-9735-ac5ed2225360",
"serie_id": "series-low", "serie_id": "series-low",
"serie_folder": "Series Low (2024)", "serie_folder": "Series Low (2024)",
"serie_name": "Series Low", "serie_name": "Series Low",
@ -72,7 +72,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "LOW", "priority": "LOW",
"added_at": "2025-11-28T14:58:37.218979Z", "added_at": "2025-11-28T15:14:50.960059Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -81,7 +81,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "c42241f8-6ffb-4ee4-bcfa-f4e388cdd8cf", "id": "b34582b3-3d3c-4883-b95f-63287b6df397",
"serie_id": "test-series", "serie_id": "test-series",
"serie_folder": "Test Series (2024)", "serie_folder": "Test Series (2024)",
"serie_name": "Test Series", "serie_name": "Test Series",
@ -92,7 +92,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.399213Z", "added_at": "2025-11-28T15:14:51.136578Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -101,7 +101,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "728a5592-0b82-4ce5-a34f-7a9c390053dc", "id": "2221eecf-e250-4f26-8e17-79b81db379bd",
"serie_id": "test-series", "serie_id": "test-series",
"serie_folder": "Test Series (2024)", "serie_folder": "Test Series (2024)",
"serie_name": "Test Series", "serie_name": "Test Series",
@ -112,7 +112,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.467671Z", "added_at": "2025-11-28T15:14:51.204136Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -121,7 +121,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "d64df138-b5b2-447a-a310-fde41a4607c8", "id": "ab779cf6-3cd5-49ca-852b-a490b61dce99",
"serie_id": "invalid-series", "serie_id": "invalid-series",
"serie_folder": "Invalid Series (2024)", "serie_folder": "Invalid Series (2024)",
"serie_name": "Invalid Series", "serie_name": "Invalid Series",
@ -132,7 +132,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.540908Z", "added_at": "2025-11-28T15:14:51.272632Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -141,7 +141,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "c87bb473-6b35-4f81-a0a8-7eec1e23756a", "id": "ecc12f47-9e1a-4cd3-a39d-df4ccd81baf4",
"serie_id": "test-series", "serie_id": "test-series",
"serie_folder": "Test Series (2024)", "serie_folder": "Test Series (2024)",
"serie_name": "Test Series", "serie_name": "Test Series",
@ -152,7 +152,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.578395Z", "added_at": "2025-11-28T15:14:51.305672Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -161,67 +161,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "babab810-6c34-4a87-aec5-f8848645f57f", "id": "109163ee-d2ce-48bb-81b3-5f6b604506e7",
"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",
"serie_id": "series-1", "serie_id": "series-1",
"serie_folder": "Series 1 (2024)", "serie_folder": "Series 1 (2024)",
"serie_name": "Series 1", "serie_name": "Series 1",
@ -232,7 +172,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.678299Z", "added_at": "2025-11-28T15:14:51.399391Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -241,7 +181,27 @@
"source_url": null "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_id": "series-3",
"serie_folder": "Series 3 (2024)", "serie_folder": "Series 3 (2024)",
"serie_name": "Series 3", "serie_name": "Series 3",
@ -252,7 +212,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.678964Z", "added_at": "2025-11-28T15:14:51.401591Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -261,7 +221,47 @@
"source_url": null "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_id": "persistent-series",
"serie_folder": "Persistent Series (2024)", "serie_folder": "Persistent Series (2024)",
"serie_name": "Persistent Series", "serie_name": "Persistent Series",
@ -272,7 +272,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.765201Z", "added_at": "2025-11-28T15:14:51.493134Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -281,7 +281,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "1b5f11b3-26d3-43ea-9615-8f951dc09bac", "id": "8ee3de44-7260-4e26-88ee-8172e45d14dd",
"serie_id": "ws-series", "serie_id": "ws-series",
"serie_folder": "WebSocket Series (2024)", "serie_folder": "WebSocket Series (2024)",
"serie_name": "WebSocket Series", "serie_name": "WebSocket Series",
@ -292,7 +292,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-28T14:58:37.835647Z", "added_at": "2025-11-28T15:14:51.573256Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -301,7 +301,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "72a71e8e-3a06-4d81-896e-d5c11956cb12", "id": "ec7f80b1-87c1-4cf4-949b-c96d6dd7a82b",
"serie_id": "workflow-series", "serie_id": "workflow-series",
"serie_folder": "Workflow Test Series (2024)", "serie_folder": "Workflow Test Series (2024)",
"serie_name": "Workflow Test Series", "serie_name": "Workflow Test Series",
@ -312,7 +312,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "HIGH", "priority": "HIGH",
"added_at": "2025-11-28T14:58:37.870997Z", "added_at": "2025-11-28T15:14:51.610620Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -323,5 +323,5 @@
], ],
"active": [], "active": [],
"failed": [], "failed": [],
"timestamp": "2025-11-28T14:58:37.880117+00:00" "timestamp": "2025-11-28T15:14:51.618568+00:00"
} }

View File

@ -112,22 +112,12 @@ For each task completed:
2. Password: `Hallo123!` 2. Password: `Hallo123!`
3. Login via browser at `http://127.0.0.1:8000/login` 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 ### Notes
- This is a simplification that removes complexity while maintaining core functionality - This is a simplification that removes complexity while maintaining core functionality
- Improves user experience with explicit manual control - Improves user experience with explicit manual control
- Easier to understand, test, and maintain - Easier to understand, test, and maintain
- Good foundation for future enhancements if needed - 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) ### Phase 4: API Layer ✅ (Completed November 28, 2025)
All API layer tasks completed: 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 ✅
--- ---
### 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) - **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`
**Objective:** Update frontend to use `key` as the primary series identifier instead of `folder`. - **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
**Steps:** - **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`
1. Open [`src/server/web/static/js/app.js`](src/server/web/static/js/app.js) - **Task 5.4: Update HTML Templates**
2. Update `seriesData` storage to index by `key` - Reviewed all templates (`index.html`, `queue.html`, etc.)
3. Update `selectedSeries` Set to use `key` instead of `folder` - No changes needed - series cards are rendered dynamically in JavaScript
4. Update `createSerieCard()`: - Static templates don't contain series data attributes
- 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
--- ---
@ -704,11 +538,11 @@ conda run -n AniWorld python -m pytest tests/integration/test_identifier_consist
- [x] Phase 2: Core Application Layer ✅ - [x] Phase 2: Core Application Layer ✅
- [x] Phase 3: Service Layer ✅ - [x] Phase 3: Service Layer ✅
- [x] Phase 4: API Layer ✅ **Completed November 28, 2025** - [x] Phase 4: API Layer ✅ **Completed November 28, 2025**
- [ ] Phase 5: Frontend - [x] Phase 5: Frontend ✅ **Completed November 28, 2025**
- [ ] Task 5.1: Update Frontend JavaScript - [x] Task 5.1: Update Frontend JavaScript
- [ ] Task 5.2: Update WebSocket Events - [x] Task 5.2: Update WebSocket Events
- [ ] Task 5.3: Update Additional Frontend JavaScript Files - [x] Task 5.3: Update Additional Frontend JavaScript Files
- [ ] Task 5.4: Update HTML Templates - [x] Task 5.4: Update HTML Templates
- [ ] Phase 6: Database Layer - [ ] Phase 6: Database Layer
- [ ] Task 6.1: Verify Database Models - [ ] Task 6.1: Verify Database Models
- [ ] Task 6.2: Update Database Services - [ ] Task 6.2: Update Database Services

View File

@ -6,8 +6,8 @@
class AniWorldApp { class AniWorldApp {
constructor() { constructor() {
this.socket = null; this.socket = null;
this.selectedSeries = new Set(); this.selectedSeries = new Set(); // Uses 'key' as identifier
this.seriesData = []; this.seriesData = []; // Series objects with 'key' as primary identifier
this.filteredSeriesData = []; this.filteredSeriesData = [];
this.isConnected = false; this.isConnected = false;
this.isDownloading = false; this.isDownloading = false;
@ -674,26 +674,27 @@ class AniWorldApp {
grid.innerHTML = dataToRender.map(serie => this.createSerieCard(serie)).join(''); 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 => { grid.querySelectorAll('.series-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => { checkbox.addEventListener('change', (e) => {
this.toggleSerieSelection(e.target.dataset.folder, e.target.checked); this.toggleSerieSelection(e.target.dataset.key, e.target.checked);
}); });
}); });
} }
createSerieCard(serie) { 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 hasMissingEpisodes = serie.missing_episodes > 0;
const canBeSelected = hasMissingEpisodes; // Only allow selection if has missing episodes const canBeSelected = hasMissingEpisodes; // Only allow selection if has missing episodes
return ` return `
<div class="series-card ${isSelected ? 'selected' : ''} ${hasMissingEpisodes ? 'has-missing' : 'complete'}" <div class="series-card ${isSelected ? 'selected' : ''} ${hasMissingEpisodes ? 'has-missing' : 'complete'}"
data-folder="${serie.folder}"> data-key="${serie.key}" data-folder="${serie.folder}">
<div class="series-card-header"> <div class="series-card-header">
<input type="checkbox" <input type="checkbox"
class="series-checkbox" class="series-checkbox"
data-folder="${serie.folder}" data-key="${serie.key}"
${isSelected ? 'checked' : ''} ${isSelected ? 'checked' : ''}
${!canBeSelected ? 'disabled' : ''}> ${!canBeSelected ? 'disabled' : ''}>
<div class="series-info"> <div class="series-info">
@ -718,20 +719,21 @@ class AniWorldApp {
`; `;
} }
toggleSerieSelection(folder, selected) { toggleSerieSelection(key, selected) {
// Only allow selection of series with missing episodes // 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) { if (!serie || serie.missing_episodes === 0) {
// Uncheck the checkbox if it was checked for a complete series // 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; if (checkbox) checkbox.checked = false;
return; return;
} }
if (selected) { if (selected) {
this.selectedSeries.add(folder); this.selectedSeries.add(key);
} else { } else {
this.selectedSeries.delete(folder); this.selectedSeries.delete(key);
} }
this.updateSelectionUI(); this.updateSelectionUI();
@ -742,45 +744,47 @@ class AniWorldApp {
const selectAllBtn = document.getElementById('select-all'); const selectAllBtn = document.getElementById('select-all');
// Get series that can be selected (have missing episodes) // 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 selectableSeriesData = this.filteredSeriesData.length > 0 ? this.filteredSeriesData : this.seriesData;
const selectableSeries = selectableSeriesData.filter(serie => serie.missing_episodes > 0); 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; 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) { if (this.selectedSeries.size === 0) {
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>'; selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
} else if (allSelectableSelected && selectableFolders.length > 0) { } else if (allSelectableSelected && selectableKeys.length > 0) {
selectAllBtn.innerHTML = '<i class="fas fa-times"></i><span>Deselect All</span>'; selectAllBtn.innerHTML = '<i class="fas fa-times"></i><span>Deselect All</span>';
} else { } else {
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>'; selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
} }
// Update card appearances // Update card appearances using 'key' as identifier
document.querySelectorAll('.series-card').forEach(card => { document.querySelectorAll('.series-card').forEach(card => {
const folder = card.dataset.folder; const key = card.dataset.key;
const isSelected = this.selectedSeries.has(folder); const isSelected = this.selectedSeries.has(key);
card.classList.toggle('selected', isSelected); card.classList.toggle('selected', isSelected);
}); });
} }
toggleSelectAll() { toggleSelectAll() {
// Get series that can be selected (have missing episodes) // 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 selectableSeriesData = this.filteredSeriesData.length > 0 ? this.filteredSeriesData : this.seriesData;
const selectableSeries = selectableSeriesData.filter(serie => serie.missing_episodes > 0); 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) { if (allSelectableSelected && this.selectedSeries.size > 0) {
// Deselect all selectable series // 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); document.querySelectorAll('.series-checkbox:not([disabled])').forEach(cb => cb.checked = false);
} else { } else {
// Select all selectable series // 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); document.querySelectorAll('.series-checkbox:not([disabled])').forEach(cb => cb.checked = true);
} }
@ -887,33 +891,35 @@ class AniWorldApp {
} }
async downloadSelected() { async downloadSelected() {
console.log('=== downloadSelected v1.1 - DEBUG VERSION ==='); console.log('=== downloadSelected v1.2 - Using key as primary identifier ===');
if (this.selectedSeries.size === 0) { if (this.selectedSeries.size === 0) {
this.showToast('No series selected', 'warning'); this.showToast('No series selected', 'warning');
return; return;
} }
try { 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('=== Starting download for selected series ===');
console.log('Selected folders:', folders); console.log('Selected keys:', selectedKeys);
console.log('seriesData:', this.seriesData); console.log('seriesData:', this.seriesData);
let totalEpisodesAdded = 0; let totalEpisodesAdded = 0;
let failedSeries = []; let failedSeries = [];
// For each selected series, get its missing episodes and add to queue // For each selected series, get its missing episodes and add to queue
for (const folder of folders) { // Use 'key' to find the series in seriesData
const serie = this.seriesData.find(s => s.folder === folder); for (const key of selectedKeys) {
const serie = this.seriesData.find(s => s.key === key);
if (!serie || !serie.episodeDict) { if (!serie || !serie.episodeDict) {
console.error('Serie not found or has no episodeDict:', folder, serie); console.error('Serie not found or has no episodeDict for key:', key, serie);
failedSeries.push(folder); failedSeries.push(key);
continue; continue;
} }
// Validate required fields // Validate required fields
if (!serie.key) { if (!serie.key) {
console.error('Serie missing key:', serie); console.error('Serie missing key:', serie);
failedSeries.push(folder); failedSeries.push(key);
continue; continue;
} }
@ -957,7 +963,7 @@ class AniWorldApp {
}); });
if (!response) { if (!response) {
failedSeries.push(folder); failedSeries.push(key);
continue; continue;
} }
@ -973,14 +979,14 @@ class AniWorldApp {
totalEpisodesAdded += episodes.length; totalEpisodesAdded += episodes.length;
} else { } else {
console.error('Failed to add to queue:', data); console.error('Failed to add to queue:', data);
failedSeries.push(folder); failedSeries.push(key);
} }
} }
// Show result message // Show result message
console.log('=== Download request complete ==='); console.log('=== Download request complete ===');
console.log('Total episodes added:', totalEpisodesAdded); console.log('Total episodes added:', totalEpisodesAdded);
console.log('Failed series:', failedSeries); console.log('Failed series (keys):', failedSeries);
if (totalEpisodesAdded > 0) { if (totalEpisodesAdded > 0) {
const message = failedSeries.length > 0 const message = failedSeries.length > 0
@ -989,7 +995,7 @@ class AniWorldApp {
this.showToast(message, 'success'); this.showToast(message, 'success');
} else { } else {
const errorDetails = failedSeries.length > 0 const errorDetails = failedSeries.length > 0
? `Failed series: ${failedSeries.join(', ')}` ? `Failed series (keys): ${failedSeries.join(', ')}`
: 'No episodes were added. Check browser console for details.'; : 'No episodes were added. Check browser console for details.';
console.error('Failed to add episodes. Details:', errorDetails); console.error('Failed to add episodes. Details:', errorDetails);
this.showToast('Failed to add episodes to queue. Check console for details.', 'error'); this.showToast('Failed to add episodes to queue. Check console for details.', 'error');

View File

@ -307,10 +307,16 @@ class TestWebSocketService:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_broadcast_download_progress(self, service, mock_websocket): 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" connection_id = "test-conn"
download_id = "download123" download_id = "download123"
progress_data = { progress_data = {
"key": "attack-on-titan",
"folder": "Attack on Titan (2013)",
"percent": 50.0, "percent": 50.0,
"speed_mbps": 2.5, "speed_mbps": 2.5,
"eta_seconds": 120, "eta_seconds": 120,
@ -325,14 +331,24 @@ class TestWebSocketService:
call_args = mock_websocket.send_json.call_args[0][0] call_args = mock_websocket.send_json.call_args[0][0]
assert call_args["type"] == "download_progress" assert call_args["type"] == "download_progress"
assert call_args["data"]["download_id"] == download_id 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 assert call_args["data"]["percent"] == 50.0
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_broadcast_download_complete(self, service, mock_websocket): 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" connection_id = "test-conn"
download_id = "download123" 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.connect(mock_websocket, connection_id)
await service._manager.join_room(connection_id, "downloads") await service._manager.join_room(connection_id, "downloads")
@ -342,13 +358,23 @@ class TestWebSocketService:
call_args = mock_websocket.send_json.call_args[0][0] call_args = mock_websocket.send_json.call_args[0][0]
assert call_args["type"] == "download_complete" assert call_args["type"] == "download_complete"
assert call_args["data"]["download_id"] == download_id 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 @pytest.mark.asyncio
async def test_broadcast_download_failed(self, service, mock_websocket): 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" connection_id = "test-conn"
download_id = "download123" 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.connect(mock_websocket, connection_id)
await service._manager.join_room(connection_id, "downloads") await service._manager.join_room(connection_id, "downloads")
@ -358,6 +384,8 @@ class TestWebSocketService:
call_args = mock_websocket.send_json.call_args[0][0] call_args = mock_websocket.send_json.call_args[0][0]
assert call_args["type"] == "download_failed" assert call_args["type"] == "download_failed"
assert call_args["data"]["download_id"] == download_id 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 @pytest.mark.asyncio
async def test_broadcast_queue_status(self, service, mock_websocket): async def test_broadcast_queue_status(self, service, mock_websocket):