added delete option
This commit is contained in:
179
Docs/DELETE_ANIME_FEATURE.md
Normal file
179
Docs/DELETE_ANIME_FEATURE.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Delete Anime Feature
|
||||
|
||||
## Overview
|
||||
|
||||
The Delete Anime feature allows authenticated users to remove an anime series from the Aniworld library. It supports three deletion modes: **database only**, **folder only**, or **both**. A mandatory typed-confirmation (`delete`) prevents accidental deletions.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### How to Delete an Anime
|
||||
|
||||
1. **Right-click** on any anime series card in the library grid.
|
||||
2. Select **"Delete Anime"** from the context menu.
|
||||
3. A confirmation modal appears with two options:
|
||||
- ☑️ **Remove from database** (recommended) — removes series and episodes from SQLite
|
||||
- ☐ **Delete folder from filesystem** — deletes the folder and all files inside
|
||||
4. **Type `delete`** in the confirmation text field to enable the Delete button.
|
||||
5. Click **Delete** to proceed.
|
||||
|
||||
### What Gets Deleted
|
||||
|
||||
| Option | Effect |
|
||||
|--------|--------|
|
||||
| Database only | Series, episodes, and queue entries removed from SQLite. Folder on disk is preserved. Downloaded episode files remain. |
|
||||
| Folder only | Entire folder and all files inside deleted from filesystem. Database record preserved with `is_downloaded=True`. |
|
||||
| Both | Full removal: database record deleted AND folder/files deleted from disk. |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend Components
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `src/server/api/anime.py` | `DELETE /api/anime/{key}` endpoint |
|
||||
| `src/server/services/anime_service.py` | `AnimeService.delete_series()` orchestrator |
|
||||
| `src/server/services/websocket_service.py` | `broadcast_series_deleted()` for real-time UI updates |
|
||||
| `src/server/database/service.py` | `AnimeSeriesService.get_folder_path()` + existing `delete()` |
|
||||
| `src/server/models/anime.py` | `DeleteSeriesRequest` / `DeleteSeriesResult` Pydantic models |
|
||||
|
||||
### Frontend Components
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `src/server/web/static/js/index/delete-modal.js` | Modal UI, confirm text validation, API calls |
|
||||
| `src/server/web/static/js/index/context-menu.js` | Right-click "Delete Anime" menu item |
|
||||
| `src/server/web/static/js/index/socket-handler.js` | `SERIES_DELETED` WebSocket event handler |
|
||||
| `src/server/web/static/js/index/series-manager.js` | `removeSeries(key)` — removes card from grid |
|
||||
| `src/server/web/static/js/index/app-init.js` | Initializes `DeleteModal` |
|
||||
| `src/server/web/static/css/components/modals.css` | Modal and context menu styles |
|
||||
| `src/server/web/templates/index.html` | Loads `delete-modal.js` before `app-init.js` |
|
||||
|
||||
### API Endpoint
|
||||
|
||||
```
|
||||
DELETE /api/anime/{key}
|
||||
```
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"delete_database": true,
|
||||
"delete_folder": false,
|
||||
"confirm_text": "delete"
|
||||
}
|
||||
```
|
||||
|
||||
**Success response (200):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"key": "attack-on-titan",
|
||||
"name": "Attack on Titan",
|
||||
"deleted_from_database": true,
|
||||
"deleted_folder": false,
|
||||
"folder_path": null,
|
||||
"database_error": null,
|
||||
"folder_error": null,
|
||||
"message": "Removed from database."
|
||||
}
|
||||
```
|
||||
|
||||
**Error responses:**
|
||||
| Status | Condition |
|
||||
|--------|-----------|
|
||||
| 400 | `confirm_text != "delete"` or neither flag is `true` |
|
||||
| 401 | Not authenticated |
|
||||
| 404 | Series `key` not found in database |
|
||||
| 500 | Unexpected server error |
|
||||
|
||||
### WebSocket Event
|
||||
|
||||
After a successful delete, the server broadcasts a `series_deleted` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "series_deleted",
|
||||
"data": {
|
||||
"key": "attack-on-titan",
|
||||
"name": "Attack on Titan"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All connected clients remove the card from their grid in real-time.
|
||||
|
||||
---
|
||||
|
||||
## Safety Mechanisms
|
||||
|
||||
### 1. Typed Confirmation
|
||||
Users must type exactly `delete` (case-sensitive) to unlock the Delete button. This prevents accidental clicks from triggering deletion.
|
||||
|
||||
### 2. Path Traversal Protection
|
||||
Before deleting a folder, `is_safe_path()` validates the path stays within the configured anime base directory. Paths outside this boundary are rejected with a `folder_error`.
|
||||
|
||||
### 3. Granular Options
|
||||
The two independent checkboxes ensure users consciously choose what to delete. Default is **database only** (recommended).
|
||||
|
||||
### 4. WebSocket Broadcast
|
||||
All clients are notified immediately when a series is deleted, keeping multiple browser sessions in sync.
|
||||
|
||||
### 5. No Shell Injection
|
||||
Series keys are never passed to shell commands. All file operations use `pathlib.Path`.
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
### Backend Logs (Python/`logging`)
|
||||
|
||||
| Event | Level | Message |
|
||||
|-------|-------|---------|
|
||||
| Delete initiated | INFO | `Delete anime initiated: key={key} delete_db={x} delete_folder={x}` |
|
||||
| Series not found | WARNING | `Delete anime failed — series not found: key={key}` |
|
||||
| Path traversal attempt | WARNING | `Delete anime blocked — path traversal attempt: key={key} path={path}` |
|
||||
| DB error | ERROR | `Delete anime DB error: key={key} error={message}` |
|
||||
| Folder delete error | ERROR | `Delete anime folder error: key={key} error={message}` |
|
||||
| Delete succeeded | INFO | `Delete anime succeeded: key={key} deleted_db={x} deleted_folder={x}` |
|
||||
|
||||
### Frontend Logs (JS/`console`)
|
||||
|
||||
| Event | Method |
|
||||
|-------|--------|
|
||||
| Modal opened | `console.info('[DeleteModal] Opening for key:', key)` |
|
||||
| Delete confirmed | `console.info('[DeleteModal] Initiating delete:', {...})` |
|
||||
| Delete succeeded | `console.info('[DeleteModal] Delete succeeded:', result)` |
|
||||
| API/network error | `console.error('[DeleteModal] Delete request failed:', err)` |
|
||||
| Series removed from grid | `console.info('[SeriesManager] Removed series from local state:', key)` |
|
||||
| WS event received | `console.info('[SocketHandler] Series deleted:', data)` |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
No new configuration options are required. The feature uses existing paths:
|
||||
- **Anime base directory**: `settings.anime_directory` (for path traversal validation)
|
||||
- **Database path**: `series_app.database_path` (for DB deletion)
|
||||
- **Queue cleanup**: `AnimeSeriesService.delete(series_key)` cascades to queue items
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
See:
|
||||
- `tests/unit/test_delete_anime_service.py` — unit tests for `AnimeService.delete_series()`
|
||||
- `tests/api/test_delete_anime_endpoint.py` — API endpoint tests including auth, validation, error cases
|
||||
- `tests/frontend/test_delete_modal.py` — frontend modal logic and DOM validation tests
|
||||
- `tests/security/test_delete_anime_security.py` — security tests for path traversal, XSS, auth bypass
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-08-16 | Feature added. `DELETE /api/anime/{key}`, right-click context menu, typed-confirmation modal, WebSocket sync. |
|
||||
Reference in New Issue
Block a user