662 lines
22 KiB
Markdown
662 lines
22 KiB
Markdown
# Plan: Delete Anime Feature
|
|
|
|
## Feature Summary
|
|
Add a right-click context menu option on anime series cards to delete an anime. Provides three deletion modes: database-only, folder-only, or both. Requires the user to type "delete" in a confirmation field.
|
|
|
|
---
|
|
|
|
## 1. Backend — Database Layer
|
|
|
|
### 1.1 `src/server/database/service.py` — Add `delete` method to `AnimeSeriesService`
|
|
|
|
```python
|
|
@staticmethod
|
|
async def delete(db: AsyncSession, series_id: int) -> bool:
|
|
"""Hard-delete an anime series and all its episodes/queue items.
|
|
|
|
Uses cascade delete configured on the AnimeSeries model
|
|
(cascade="all, delete-orphan" on episodes and download_items).
|
|
|
|
Args:
|
|
db: Database session
|
|
series_id: Primary key (id) of the AnimeSeries to delete
|
|
|
|
Returns:
|
|
True if a row was deleted, False if not found
|
|
|
|
Raises:
|
|
AnimeServiceError: On database errors
|
|
"""
|
|
```
|
|
|
|
**Implementation notes:**
|
|
- Uses `select(delete(...).where(...))` pattern matching existing codebase style
|
|
- Logs series key + name before deletion for audit trail
|
|
- Returns `True`/`False` (not an exception) when series not found — caller decides response code
|
|
- Wraps in try/except, logs error, re-raises as `AnimeServiceError`
|
|
|
|
### 1.2 `src/server/database/service.py` — Add `get_folder_path` helper to `AnimeSeriesService`
|
|
|
|
```python
|
|
@staticmethod
|
|
async def get_folder_path(db: AsyncSession, series_key: str) -> str | None:
|
|
"""Get the filesystem folder path for a series by its key.
|
|
|
|
Args:
|
|
db: Database session
|
|
series_key: Provider key (e.g. "attack-on-titan")
|
|
|
|
Returns:
|
|
Folder path string, or None if series not found
|
|
"""
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Backend — AnimeService
|
|
|
|
### 2.1 `src/server/services/anime_service.py` — Add `delete_series` method
|
|
|
|
```python
|
|
async def delete_series(
|
|
self,
|
|
key: str,
|
|
delete_database: bool = True,
|
|
delete_folder: bool = False,
|
|
) -> DeleteSeriesResult:
|
|
"""Delete an anime series from DB, filesystem, or both.
|
|
|
|
Args:
|
|
key: Series key (primary identifier)
|
|
delete_database: If True, remove from database (default True)
|
|
delete_folder: If True, remove folder from filesystem (default False)
|
|
|
|
Returns:
|
|
DeleteSeriesResult with success status, what was deleted, errors
|
|
|
|
Raises:
|
|
AnimeServiceError: On critical failures
|
|
"""
|
|
```
|
|
|
|
**`DeleteSeriesResult` Pydantic model (add to `src/server/models/anime.py`):**
|
|
|
|
```python
|
|
class DeleteSeriesResult(BaseModel):
|
|
success: bool
|
|
key: str
|
|
name: str
|
|
deleted_from_database: bool
|
|
deleted_folder: bool
|
|
folder_path: str | None
|
|
database_error: str | None
|
|
folder_error: str | None
|
|
message: str
|
|
```
|
|
|
|
**Step-by-step inside `delete_series`:**
|
|
|
|
1. **Log start** — `logger.info("Delete series requested key=%s db=%s folder=%s", key, delete_database, delete_folder)`
|
|
2. **Fetch series from DB** to get `id`, `folder`, `name`
|
|
3. **If `delete_database=True`**:
|
|
a. Call `AnimeSeriesService.delete(db, series_id)`
|
|
b. Log success/failure
|
|
c. Invalidate `_cached_list_missing` LRU cache
|
|
d. Broadcast `series_deleted` WebSocket event
|
|
4. **If `delete_folder=True`**:
|
|
a. Validate folder path with `is_safe_path(self._directory, folder_path)` — reject if outside base directory
|
|
b. Use `shutil.rmtree(folder_path)` to delete the folder
|
|
c. Log success/failure
|
|
5. **Log completion** — `logger.info("Delete series completed key=%s results=%s", key, result)`
|
|
6. Return `DeleteSeriesResult`
|
|
|
|
**Error handling:**
|
|
- DB errors during folder delete → log but don't fail the whole operation
|
|
- Folder delete errors → log, attach to result, don't rollback DB delete
|
|
- Series not found → return `DeleteSeriesResult(success=False, message="Series not found")`
|
|
|
|
### 2.2 `src/server/services/anime_service.py` — Add `broadcast_series_deleted`
|
|
|
|
```python
|
|
async def _broadcast_series_deleted(self, key: str, name: str) -> None:
|
|
"""Broadcast series_deleted event via WebSocket."""
|
|
```
|
|
|
|
Mirrors existing `_broadcast_series_updated` pattern.
|
|
|
|
---
|
|
|
|
## 3. Backend — API Layer
|
|
|
|
### 3.1 `src/server/api/anime.py` — Add request/response models
|
|
|
|
```python
|
|
class DeleteSeriesRequest(BaseModel):
|
|
"""Request payload for DELETE /api/anime/{key}."""
|
|
delete_database: bool = Field(
|
|
default=True,
|
|
description="Whether to remove the series from the database"
|
|
)
|
|
delete_folder: bool = Field(
|
|
default=False,
|
|
description="Whether to delete the series folder from filesystem"
|
|
)
|
|
confirm_text: str = Field(
|
|
...,
|
|
description="Must be exactly 'delete' to confirm"
|
|
)
|
|
|
|
class DeleteSeriesResponse(BaseModel):
|
|
"""Response payload for DELETE /api/anime/{key}."""
|
|
success: bool
|
|
key: str
|
|
name: str
|
|
deleted_from_database: bool
|
|
deleted_folder: bool
|
|
folder_path: str | None
|
|
database_error: str | None
|
|
folder_error: str | None
|
|
message: str
|
|
```
|
|
|
|
### 3.2 `src/server/api/anime.py` — Add DELETE endpoint
|
|
|
|
```python
|
|
@router.delete("/{key}", response_model=DeleteSeriesResponse)
|
|
async def delete_anime(
|
|
key: str,
|
|
request: DeleteSeriesRequest,
|
|
_auth: dict = Depends(require_auth),
|
|
anime_service: AnimeService = Depends(get_anime_service),
|
|
) -> DeleteSeriesResponse:
|
|
"""Delete an anime series from database, filesystem, or both.
|
|
|
|
Requires typing 'delete' in the confirm_text field to prevent accidents.
|
|
|
|
Args:
|
|
key: Series key (from URL path)
|
|
request: DeleteSeriesRequest with options and confirmation
|
|
_auth: Ensures the caller is authenticated
|
|
anime_service: AnimeService instance
|
|
|
|
Returns:
|
|
DeleteSeriesResponse with outcome details
|
|
|
|
Raises:
|
|
HTTPException(400): If confirm_text is not exactly 'delete'
|
|
HTTPException(404): If series not found
|
|
HTTPException(500): On unexpected errors
|
|
"""
|
|
```
|
|
|
|
**Validation:**
|
|
- `confirm_text` must be exactly `"delete"` (case-insensitive? No — exact match to be strict)
|
|
- If `delete_folder=True` but folder doesn't exist → log warning, continue with DB delete
|
|
- If neither `delete_database` nor `delete_folder` is True → return 400
|
|
|
|
### 3.3 Update constants — No changes needed to `constants.js` for the endpoint path
|
|
|
|
The existing `API.ANIME_LIST` pattern is `/api/anime` — DELETE `/api/anime/{key}` follows REST conventions.
|
|
|
|
---
|
|
|
|
## 4. Frontend — Context Menu
|
|
|
|
### 4.1 `src/server/web/static/js/index/context-menu.js` — Add delete option
|
|
|
|
```javascript
|
|
// In the show() function, add a divider and delete option:
|
|
menuElement.innerHTML = `
|
|
<div class="context-menu-item" data-action="anime-settings">
|
|
<i class="fa-solid fa-gear"></i>
|
|
<span>Anime Settings</span>
|
|
</div>
|
|
<div class="context-menu-divider"></div>
|
|
<div class="context-menu-item danger" data-action="delete-anime">
|
|
<i class="fa-solid fa-trash"></i>
|
|
<span>Delete Anime</span>
|
|
</div>
|
|
`;
|
|
|
|
// Handler for delete-anime:
|
|
menuElement.querySelector('[data-action="delete-anime"]').addEventListener('click', function() {
|
|
const key = currentSeriesKey;
|
|
hide();
|
|
AniWorld.DeleteModal && AniWorld.DeleteModal.show(key);
|
|
});
|
|
```
|
|
|
|
### 4.2 Create `src/server/web/static/js/index/delete-modal.js`
|
|
|
|
New module for the confirmation modal. Module structure mirrors `anime-settings.js`.
|
|
|
|
**Features:**
|
|
- `show(key)` — opens modal with series info populated
|
|
- `hide()` — closes and resets modal
|
|
- Shows series name and key being deleted
|
|
- Three checkboxes: `☐ Remove from database` (default checked), `☐ Delete folder` (default unchecked)
|
|
- Confirmation text field: user must type exactly `delete`
|
|
- Delete button: disabled until confirmation text matches
|
|
- Error display area
|
|
- Keyboard: Escape closes, Enter submits if valid
|
|
|
|
**Modal HTML structure** (inline in JS, no new HTML file needed):
|
|
|
|
```html
|
|
<div id="delete-modal" class="modal hidden">
|
|
<div class="modal-backdrop"></div>
|
|
<div class="modal-content">
|
|
<h2>Delete Anime</h2>
|
|
<p id="delete-modal-series-name"></p>
|
|
<p id="delete-modal-series-key"></p>
|
|
|
|
<label>
|
|
<input type="checkbox" id="delete-db-checkbox" checked>
|
|
Remove from database (recommended)
|
|
</label>
|
|
<label>
|
|
<input type="checkbox" id="delete-folder-checkbox">
|
|
Delete folder from filesystem
|
|
</label>
|
|
<p class="warning">This will permanently delete the folder and all its files!</p>
|
|
|
|
<label for="delete-confirm-input">
|
|
Type <strong>delete</strong> to confirm:
|
|
</label>
|
|
<input type="text" id="delete-confirm-input" placeholder="delete">
|
|
|
|
<div id="delete-error" class="error-message hidden"></div>
|
|
|
|
<div class="modal-actions">
|
|
<button id="delete-cancel-btn">Cancel</button>
|
|
<button id="delete-confirm-btn" disabled>Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
**CSS** — add to existing CSS files:
|
|
- `.context-menu-item.danger { color: var(--color-danger, #dc3545); }`
|
|
- `.context-menu-divider { height: 1px; background: var(--color-border); margin: 4px 0; }`
|
|
- `.warning { color: var(--color-warning, #ffc107); font-size: 0.875em; }`
|
|
- Modal styles (`.modal`, `.modal-backdrop`, `.modal-content`) — reuse existing modal CSS if present, or add new
|
|
|
|
**API call on confirm:**
|
|
```javascript
|
|
const response = await AniWorld.ApiClient.request(`/api/anime/${encodeURIComponent(key)}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
delete_database: document.getElementById('delete-db-checkbox').checked,
|
|
delete_folder: document.getElementById('delete-folder-checkbox').checked,
|
|
confirm_text: document.getElementById('delete-confirm-input').value
|
|
})
|
|
});
|
|
```
|
|
|
|
**On success:**
|
|
- Show success toast
|
|
- Close modal
|
|
- Reload series grid (or remove card from DOM directly)
|
|
|
|
**On error:**
|
|
- Show error message in modal
|
|
|
|
### 4.3 `src/server/web/static/js/index/app-init.js` — Initialize delete modal
|
|
|
|
Add initialization call:
|
|
```javascript
|
|
if (AniWorld.DeleteModal) {
|
|
AniWorld.DeleteModal.init();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. WebSocket Event
|
|
|
|
### 5.1 `src/server/services/websocket_service.py` — Add `broadcast_series_deleted`
|
|
|
|
```python
|
|
async def broadcast_series_deleted(self, key: str, name: str) -> None:
|
|
"""Broadcast a series_deleted event.
|
|
|
|
Args:
|
|
key: Series key that was deleted
|
|
name: Series name for display
|
|
"""
|
|
```
|
|
|
|
### 5.2 `src/server/web/static/js/index/socket-handler.js` — Handle `series_deleted`
|
|
|
|
Handle the new WebSocket event to remove the deleted card from the UI in real-time:
|
|
|
|
```javascript
|
|
case AniWorld.Constants.WS_EVENTS.SERIES_DELETED:
|
|
if (AniWorld.SeriesManager) {
|
|
AniWorld.SeriesManager.removeSeries(data.key);
|
|
}
|
|
break;
|
|
```
|
|
|
|
### 5.3 `src/server/web/static/js/shared/constants.js` — Add event constant
|
|
|
|
```javascript
|
|
SERIES_DELETED: 'series_deleted',
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Logging
|
|
|
|
### 6.1 Backend logging points (all use Python `logging.getLogger(__name__)`):
|
|
|
|
| Event | Level | Message format |
|
|
|-------|-------|----------------|
|
|
| Delete requested | `info` | `"Delete series requested: key=%s delete_database=%s delete_folder=%s"` |
|
|
| Series not found | `warning` | `"Delete series failed - not found: key=%s"` |
|
|
| DB delete success | `info` | `"Deleted series from database: key=%s name=%s id=%d"` |
|
|
| DB delete failure | `error` | `"Failed to delete series from database: key=%s error=%s"` |
|
|
| Folder delete start | `info` | `"Deleting series folder: key=%s path=%s"` |
|
|
| Folder delete success | `info` | `"Deleted series folder: key=%s path=%s"` |
|
|
| Folder delete failure | `error` | `"Failed to delete series folder: key=%s path=%s error=%s"` |
|
|
| Path traversal blocked | `warning` | `"Blocked unsafe folder delete attempt: key=%s path=%s base=%s"` |
|
|
| Delete completed | `info` | `"Delete series completed: key=%s db=%s folder=%s"` |
|
|
|
|
### 6.2 Frontend logging points (console.log / console.error):
|
|
|
|
| Event | Level |
|
|
|-------|-------|
|
|
| Delete modal opened | `console.info` |
|
|
| Delete API call initiated | `console.info` |
|
|
| Delete success | `console.info` |
|
|
| Delete API error | `console.error` |
|
|
| Validation failure (confirm_text) | `console.warn` |
|
|
|
|
---
|
|
|
|
## 7. Documentation
|
|
|
|
### 7.1 Create `docs/delete-anime-feature.md`
|
|
|
|
```markdown
|
|
# Delete Anime Feature
|
|
|
|
## Overview
|
|
Allows authenticated users to delete an anime series from the database,
|
|
filesystem, or both via the right-click context menu on series cards.
|
|
|
|
## Safety Mechanisms
|
|
|
|
### Confirmation Required
|
|
Users must type exactly `delete` in a confirmation field before deletion
|
|
proceeds. This prevents accidental clicks.
|
|
|
|
### Selective Deletion
|
|
Two independent options:
|
|
- **Remove from database**: Removes the series and all its episodes/queue
|
|
items from the SQLite database. The folder on disk is preserved.
|
|
- **Delete folder**: Removes the series folder and ALL files inside it
|
|
from the filesystem. This cannot be undone.
|
|
|
|
### Path Traversal Protection
|
|
Folder deletion validates the target path is within the configured
|
|
`directory_to_search` base directory before attempting deletion.
|
|
|
|
## API
|
|
|
|
### DELETE /api/anime/{key}
|
|
|
|
**Request body:**
|
|
```json
|
|
{
|
|
"delete_database": true,
|
|
"delete_folder": false,
|
|
"confirm_text": "delete"
|
|
}
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"key": "attack-on-titan",
|
|
"name": "Attack on Titan",
|
|
"deleted_from_database": true,
|
|
"deleted_folder": false,
|
|
"folder_path": "/anime/Attack on Titan (2013)",
|
|
"database_error": null,
|
|
"folder_error": null,
|
|
"message": "Series deleted from database successfully."
|
|
}
|
|
```
|
|
|
|
**Error responses:**
|
|
- `400 Bad Request`: confirm_text != "delete", or neither delete option selected
|
|
- `401 Unauthorized`: Missing or invalid auth token
|
|
- `404 Not Found`: Series key does not exist
|
|
- `500 Internal Server Error`: Unexpected error
|
|
|
|
## Events
|
|
|
|
### WebSocket: series_deleted
|
|
Broadcast to all connected clients when a series is deleted.
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"event": "series_deleted",
|
|
"key": "attack-on-titan",
|
|
"name": "Attack on Titan"
|
|
}
|
|
```
|
|
|
|
## Permissions
|
|
Requires authentication. Only authenticated users can delete anime.
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Tests
|
|
|
|
### 8.1 Unit Tests — `tests/unit/test_anime_service.py`
|
|
|
|
Add new test class:
|
|
|
|
```python
|
|
class TestDeleteSeries:
|
|
"""Test delete_series operation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_database_only_success(self, anime_service, mock_series_app):
|
|
"""Test deleting a series from database only."""
|
|
# Setup: create mock series in DB
|
|
# Assert: delete_database=True, delete_folder=False
|
|
# Assert: cache invalidated
|
|
# Assert: WebSocket broadcast called
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_folder_only(self, anime_service, mock_series_app, tmp_path):
|
|
"""Test deleting only the folder."""
|
|
# Setup: create series with folder on disk
|
|
# Assert: folder deleted from filesystem
|
|
# Assert: DB record still exists
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_both(self, anime_service, mock_series_app, tmp_path):
|
|
"""Test deleting both DB record and folder."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_nonexistent(self, anime_service):
|
|
"""Test deleting a series that doesn't exist returns success=False."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_folder_path_traversal_blocked(self, anime_service):
|
|
"""Test that path traversal attempts are blocked and logged."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_folder_not_found_continues(self, anime_service, mock_series_app):
|
|
"""Test that missing folder doesn't fail the DB delete."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_invalid_confirm_text(self, anime_service):
|
|
"""Test API rejects non-matching confirm_text."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_no_options_selected(self, anime_service):
|
|
"""Test API rejects when neither option is True."""
|
|
```
|
|
|
|
### 8.2 API Endpoint Tests — `tests/api/test_anime_endpoints.py`
|
|
|
|
Add tests for `DELETE /api/anime/{key}`:
|
|
|
|
```python
|
|
class TestDeleteAnimeEndpoints:
|
|
"""Tests for DELETE /api/anime/{key}."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_requires_auth(self, client):
|
|
"""Test that unauthenticated requests are rejected."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_requires_confirm_text(self, client, auth_headers, test_series):
|
|
"""Test that missing confirm_text returns 400."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_wrong_confirm_text(self, client, auth_headers, test_series):
|
|
"""Test that wrong confirm_text returns 400."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_database_only(self, client, auth_headers, test_series):
|
|
"""Test database-only deletion."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_folder_only(self, client, auth_headers, test_series):
|
|
"""Test folder-only deletion."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_both(self, client, auth_headers, test_series):
|
|
"""Test deletion of both DB and folder."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_nonexistent_returns_404(self, client, auth_headers):
|
|
"""Test deleting non-existent series."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_folder_outside_base_rejected(self, client, auth_headers, test_series):
|
|
"""Test that path traversal is blocked."""
|
|
```
|
|
|
|
### 8.3 Frontend Tests — `tests/frontend/test_existing_ui_integration.py`
|
|
|
|
Add integration tests for delete modal:
|
|
|
|
```javascript
|
|
describe('Delete Anime Modal', () => {
|
|
it('should open on context menu delete click');
|
|
it('should require typing delete to enable button');
|
|
it('should call DELETE API on confirm');
|
|
it('should show error on API failure');
|
|
it('should close on cancel');
|
|
it('should close on Escape key');
|
|
it('should disable confirm until text matches');
|
|
});
|
|
```
|
|
|
|
### 8.4 Security Tests — `tests/security/test_input_validation.py`
|
|
|
|
```python
|
|
class TestDeleteValidation:
|
|
"""Security tests for delete endpoint input validation."""
|
|
|
|
def test_path_traversal_in_folder_delete(self, client, auth_headers):
|
|
"""Ensure folder delete cannot escape base directory."""
|
|
|
|
def test_missing_confirm_text_rejected(self, client, auth_headers):
|
|
"""Ensure confirm_text is validated."""
|
|
|
|
def test_empty_key_rejected(self, client, auth_headers):
|
|
"""Ensure empty series key is rejected."""
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Step-by-Step Implementation Order
|
|
|
|
### Phase 1: Backend Core
|
|
1. **Add `AnimeSeriesService.delete()` and `get_folder_path()`** to `src/server/database/service.py`
|
|
2. **Add `DeleteSeriesResult` Pydantic model** to `src/server/models/anime.py`
|
|
3. **Add `DeleteSeriesRequest` and `DeleteSeriesResponse`** to `src/server/models/anime.py`
|
|
4. **Add `delete_series()` and `_broadcast_series_deleted()`** to `src/server/services/anime_service.py`
|
|
5. **Add `broadcast_series_deleted()`** to `src/server/services/websocket_service.py`
|
|
6. **Add `DELETE /api/anime/{key}` endpoint** to `src/server/api/anime.py`
|
|
7. **Add `SERIES_DELETED` constant** to `src/server/web/static/js/shared/constants.js`
|
|
|
|
### Phase 2: Frontend
|
|
8. **Add CSS styles** for context menu danger item, divider, delete modal
|
|
9. **Create `src/server/web/static/js/index/delete-modal.js`** with full modal implementation
|
|
10. **Update `src/server/web/static/js/index/context-menu.js`** to add delete option
|
|
11. **Update `src/server/web/static/js/index/app-init.js`** to initialize modal
|
|
12. **Update `src/server/web/static/js/index/socket-handler.js`** to handle `series_deleted` event
|
|
13. **Update `src/server/web/static/js/index/series-manager.js`** — add `removeSeries(key)` method
|
|
|
|
### Phase 3: Tests
|
|
14. **Add unit tests** in `tests/unit/test_anime_service.py`
|
|
15. **Add API endpoint tests** in `tests/api/test_anime_endpoints.py`
|
|
16. **Add frontend integration tests** in `tests/frontend/`
|
|
17. **Add security validation tests** in `tests/security/test_input_validation.py`
|
|
|
|
### Phase 4: Documentation
|
|
18. **Create `docs/delete-anime-feature.md`**
|
|
19. **Update `docs/README.md`** or main docs index if it exists
|
|
|
|
---
|
|
|
|
## Key Files to Modify
|
|
|
|
| File | Change Type |
|
|
|------|-------------|
|
|
| `src/server/database/service.py` | Add 2 methods |
|
|
| `src/server/models/anime.py` | Add 2 Pydantic models |
|
|
| `src/server/services/anime_service.py` | Add 2 methods |
|
|
| `src/server/services/websocket_service.py` | Add 1 method |
|
|
| `src/server/api/anime.py` | Add 2 models + 1 endpoint |
|
|
| `src/server/web/static/js/shared/constants.js` | Add 1 constant |
|
|
| `src/server/web/static/js/index/context-menu.js` | Add menu item + handler |
|
|
| `src/server/web/static/js/index/delete-modal.js` | **New file** |
|
|
| `src/server/web/static/js/index/app-init.js` | Add init call |
|
|
| `src/server/web/static/js/index/socket-handler.js` | Handle new event |
|
|
| `src/server/web/static/js/index/series-manager.js` | Add `removeSeries()` |
|
|
| CSS files | Add modal/context menu styles |
|
|
| `tests/unit/test_anime_service.py` | Add test class |
|
|
| `tests/api/test_anime_endpoints.py` | Add endpoint tests |
|
|
| `tests/frontend/test_existing_ui_integration.py` | Add frontend tests |
|
|
| `tests/security/test_input_validation.py` | Add security tests |
|
|
| `docs/delete-anime-feature.md` | **New file** |
|
|
|
|
---
|
|
|
|
## Verification Steps
|
|
|
|
After implementation, verify by running:
|
|
```bash
|
|
# Backend tests
|
|
pytest tests/unit/test_anime_service.py::TestDeleteSeries -v
|
|
pytest tests/api/test_anime_endpoints.py::TestDeleteAnimeEndpoints -v
|
|
pytest tests/security/test_input_validation.py::TestDeleteValidation -v
|
|
|
|
# Frontend tests
|
|
npm run test
|
|
|
|
# Manual verification:
|
|
# 1. Right-click a series card → "Delete Anime" option appears
|
|
# 2. Clicking it opens the confirmation modal
|
|
# 3. Without typing "delete", the button is disabled
|
|
# 4. Typing "delete" enables the button
|
|
# 5. Selecting "Remove from database" and confirming deletes the series
|
|
# 6. Selecting "Delete folder" deletes the folder from disk
|
|
# 7. Selecting both deletes both DB record and folder
|
|
# 8. After deletion, the card is removed from the grid in real-time (WebSocket)
|
|
```
|