Fix: Add graceful download cancellation on Ctrl+C
- Add cancellation flag to AniworldLoader with request_cancel/reset_cancel/is_cancelled methods - Update base_provider.Loader interface with cancellation abstract methods - Integrate cancellation check in YT-DLP progress hooks - Add request_download_cancel method to SeriesApp and AnimeService - Update DownloadService.stop() to request cancellation before shutdown - Clean up temp files on cancellation
This commit is contained in:
parent
778d16b21a
commit
08f816a954
Binary file not shown.
Binary file not shown.
@ -17,7 +17,8 @@
|
||||
"keep_days": 30
|
||||
},
|
||||
"other": {
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$MoYQ4tx7D8FY631P6b3Xeg$Lkk9WJI928F4EzBrUe1VnRD9LgKzy31zoygoIGQwqKY"
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$aq3VOsfY21sLwfgfQwghJA$d33KHoETVV5.zpCfR.BqM.ICe.DwjDcfATrsrsZ/3yM",
|
||||
"anime_directory": "/mnt/server/serien/Serien/"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"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$4tyb09q7F.I8JwSgtPYe4w$MpmQLy0b1tYvjqNwwbHy4b59AxtjZdQ8eqrYlbrwmO4"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"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$vBdCKMUYA.Dc.7.3NqbUGg$2GOV4HuUcrl8Dolk3bzmXsOqG/xC/rCmzd1G2lIWtog"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"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$gvDe27t3TilFiHHOuZeSMg$zEPyA6XcqVVTz7raeXZnMtGt/Q5k8ZCl204K0hx5z0w"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"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$1pqTMkaoFSLEWKsVAmBsDQ$DHVcHMFFYJxzYmc.7LnDru61mYtMv9PMoxPgfuKed/c"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"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$ndM6hxDC.F8LYUxJCSGEEA$UHGXMaEruWVgpRp8JI/siGETH8gOb20svhjy9plb0Wo"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -105,786 +105,3 @@ For each task completed:
|
||||
- [ ] Take the next task
|
||||
|
||||
---
|
||||
|
||||
## Task: Refactor CSS & JavaScript Files (Single Responsibility Principle) ✅ COMPLETED
|
||||
|
||||
### Status: COMPLETED
|
||||
|
||||
The CSS and JavaScript files have been successfully refactored into modular structures.
|
||||
|
||||
### Summary of Changes
|
||||
|
||||
**CSS Refactoring:**
|
||||
|
||||
- Created 17 modular CSS files organized into `base/`, `components/`, `pages/`, and `utilities/` directories
|
||||
- `styles.css` now serves as an entry point with @import statements
|
||||
- All CSS files under 500 lines (largest: helpers.css at 368 lines)
|
||||
- Total: 3,146 lines across 17 files
|
||||
|
||||
**JavaScript Refactoring:**
|
||||
|
||||
- Created 6 shared utility modules in `js/shared/`
|
||||
- Created 11 index page modules in `js/index/`
|
||||
- Created 5 queue page modules in `js/queue/`
|
||||
- Uses IIFE pattern with `AniWorld` namespace for browser compatibility
|
||||
- All JS files under 500 lines (largest: scan-manager.js at 439 lines)
|
||||
- Total: 4,795 lines across 22 modules
|
||||
|
||||
**Updated Files:**
|
||||
|
||||
- `index.html` - Updated script tags for modular JS
|
||||
- `queue.html` - Updated script tags for modular JS
|
||||
- `test_static_files.py` - Updated tests for modular architecture
|
||||
- `test_template_integration.py` - Updated tests for new JS structure
|
||||
- `ARCHITECTURE.md` - Added frontend architecture documentation
|
||||
|
||||
**Old Files (kept for reference):**
|
||||
|
||||
- `app.js` - Original monolithic file (can be deleted)
|
||||
- `queue.js` - Original monolithic file (can be deleted)
|
||||
|
||||
### Original Overview
|
||||
|
||||
Split monolithic `styles.css` (~2,135 lines), `app.js` (~2,305 lines), and `queue.js` (~993 lines) into smaller, focused files following the Single Responsibility Principle. Maximum 500 lines per file. All changes must maintain full backward compatibility with existing templates.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Server is running and functional before starting
|
||||
- All existing functionality works (login, index, queue pages)
|
||||
- Backup current files before making changes
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Analyze Current File Structure
|
||||
|
||||
**Objective**: Understand the current codebase before making changes.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. Open and read `src/server/web/static/css/styles.css`
|
||||
2. Open and read `src/server/web/static/js/app.js`
|
||||
3. Open and read `src/server/web/static/js/queue.js`
|
||||
4. Open and read `src/server/web/templates/index.html`
|
||||
5. Open and read `src/server/web/templates/queue.html`
|
||||
6. Open and read `src/server/web/templates/login.html`
|
||||
7. Document all CSS sections (look for comment headers)
|
||||
8. Document all JavaScript functions and their dependencies
|
||||
9. Identify shared utilities vs page-specific code
|
||||
|
||||
**Deliverable**: A mental map of all functions, styles, and their relationships.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create CSS Directory Structure
|
||||
|
||||
**Objective**: Set up the new CSS file organization.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. Create directory: `src/server/web/static/css/base/`
|
||||
2. Create directory: `src/server/web/static/css/components/`
|
||||
3. Create directory: `src/server/web/static/css/pages/`
|
||||
4. Create directory: `src/server/web/static/css/utilities/`
|
||||
|
||||
**File Structure to Create**:
|
||||
|
||||
```
|
||||
src/server/web/static/css/
|
||||
├── styles.css # Main entry point with @import statements
|
||||
├── base/
|
||||
│ ├── variables.css # CSS custom properties (colors, fonts, spacing)
|
||||
│ ├── reset.css # CSS reset and normalize styles
|
||||
│ └── typography.css # Font styles, headings, text utilities
|
||||
├── components/
|
||||
│ ├── buttons.css # All button styles
|
||||
│ ├── cards.css # Card and panel components
|
||||
│ ├── forms.css # Form inputs, labels, validation styles
|
||||
│ ├── modals.css # Modal and overlay styles
|
||||
│ ├── navigation.css # Header, nav, sidebar styles
|
||||
│ ├── progress.css # Progress bars, loading indicators
|
||||
│ ├── notifications.css # Toast, alerts, messages
|
||||
│ └── tables.css # Table and list styles
|
||||
├── pages/
|
||||
│ ├── login.css # Login page specific styles
|
||||
│ ├── index.css # Index/library page specific styles
|
||||
│ └── queue.css # Queue page specific styles
|
||||
└── utilities/
|
||||
├── animations.css # Keyframes and animation classes
|
||||
├── responsive.css # Media queries and breakpoints
|
||||
└── helpers.css # Utility classes (hidden, flex, spacing)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Split styles.css into Modular Files
|
||||
|
||||
**Objective**: Extract styles from `styles.css` into appropriate module files.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Extract variables.css**:
|
||||
|
||||
- Find all `:root` CSS custom properties
|
||||
- Extract color variables, font variables, spacing variables
|
||||
- Include dark mode variables (`.dark-mode` or `[data-theme="dark"]`)
|
||||
|
||||
2. **Extract reset.css**:
|
||||
|
||||
- Extract `*`, `body`, `html` base resets
|
||||
- Extract box-sizing rules
|
||||
- Extract default margin/padding resets
|
||||
|
||||
3. **Extract typography.css**:
|
||||
|
||||
- Extract `h1-h6` styles
|
||||
- Extract paragraph, link, text styles
|
||||
- Extract font-related utility classes
|
||||
|
||||
4. **Extract buttons.css**:
|
||||
|
||||
- Find all `.btn`, `button`, `.button` related styles
|
||||
- Include hover, active, disabled states
|
||||
- Include button variants (primary, secondary, danger, etc.)
|
||||
|
||||
5. **Extract cards.css**:
|
||||
|
||||
- Extract `.card`, `.panel`, `.box` related styles
|
||||
- Include card headers, bodies, footers
|
||||
|
||||
6. **Extract forms.css**:
|
||||
|
||||
- Extract `input`, `select`, `textarea` styles
|
||||
- Extract `.form-group`, `.form-control` styles
|
||||
- Extract validation states (error, success)
|
||||
|
||||
7. **Extract modals.css**:
|
||||
|
||||
- Extract `.modal`, `.overlay`, `.dialog` styles
|
||||
- Include backdrop styles
|
||||
- Include modal animations
|
||||
|
||||
8. **Extract navigation.css**:
|
||||
|
||||
- Extract `header`, `nav`, `.navbar` styles
|
||||
- Extract menu and navigation link styles
|
||||
|
||||
9. **Extract progress.css**:
|
||||
|
||||
- Extract `.progress`, `.progress-bar` styles
|
||||
- Extract loading spinners and indicators
|
||||
|
||||
10. **Extract notifications.css**:
|
||||
|
||||
- Extract `.toast`, `.alert`, `.notification` styles
|
||||
- Include success, error, warning, info variants
|
||||
|
||||
11. **Extract tables.css**:
|
||||
|
||||
- Extract `table`, `.table` styles
|
||||
- Extract list styles if table-like
|
||||
|
||||
12. **Extract page-specific styles**:
|
||||
|
||||
- `login.css`: Styles only used on login page
|
||||
- `index.css`: Styles only used on index/library page (series cards, search)
|
||||
- `queue.css`: Styles only used on queue page (queue items, download status)
|
||||
|
||||
13. **Extract animations.css**:
|
||||
|
||||
- Extract all `@keyframes` rules
|
||||
- Extract animation utility classes
|
||||
|
||||
14. **Extract responsive.css**:
|
||||
|
||||
- Extract all `@media` queries
|
||||
- Organize by breakpoint
|
||||
|
||||
15. **Extract helpers.css**:
|
||||
|
||||
- Extract utility classes (.hidden, .flex, .text-center, etc.)
|
||||
- Extract spacing utilities
|
||||
|
||||
16. **Update main styles.css**:
|
||||
- Replace all content with `@import` statements
|
||||
- Order imports correctly (variables first, then reset, then components)
|
||||
|
||||
**Import Order in styles.css**:
|
||||
|
||||
```css
|
||||
/* Base */
|
||||
@import "base/variables.css";
|
||||
@import "base/reset.css";
|
||||
@import "base/typography.css";
|
||||
|
||||
/* Components */
|
||||
@import "components/buttons.css";
|
||||
@import "components/cards.css";
|
||||
@import "components/forms.css";
|
||||
@import "components/modals.css";
|
||||
@import "components/navigation.css";
|
||||
@import "components/progress.css";
|
||||
@import "components/notifications.css";
|
||||
@import "components/tables.css";
|
||||
|
||||
/* Pages */
|
||||
@import "pages/login.css";
|
||||
@import "pages/index.css";
|
||||
@import "pages/queue.css";
|
||||
|
||||
/* Utilities (load last to allow overrides) */
|
||||
@import "utilities/animations.css";
|
||||
@import "utilities/responsive.css";
|
||||
@import "utilities/helpers.css";
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
|
||||
- Start the server
|
||||
- Check login page styling
|
||||
- Check index page styling
|
||||
- Check queue page styling
|
||||
- Verify dark mode toggle works
|
||||
- Verify responsive design works
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Create JavaScript Directory Structure
|
||||
|
||||
**Objective**: Set up the new JavaScript file organization.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. Create directory: `src/server/web/static/js/shared/`
|
||||
2. Create directory: `src/server/web/static/js/index/`
|
||||
3. Create directory: `src/server/web/static/js/queue/`
|
||||
|
||||
**File Structure to Create**:
|
||||
|
||||
```
|
||||
src/server/web/static/js/
|
||||
├── app.js # Main entry point for index page
|
||||
├── queue.js # Main entry point for queue page
|
||||
├── shared/
|
||||
│ ├── auth.js # Authentication utilities
|
||||
│ ├── api-client.js # HTTP request wrapper with auth
|
||||
│ ├── websocket-client.js # WebSocket connection management
|
||||
│ ├── theme.js # Dark/light mode management
|
||||
│ ├── ui-utils.js # Toast, loading overlay, formatters
|
||||
│ └── constants.js # Shared constants and config
|
||||
├── index/
|
||||
│ ├── series-manager.js # Series loading, filtering, rendering
|
||||
│ ├── search.js # Search functionality
|
||||
│ ├── scan-manager.js # Library scan operations
|
||||
│ ├── config-manager.js # Configuration modal handling
|
||||
│ └── selection.js # Series/episode selection logic
|
||||
└── queue/
|
||||
├── queue-api.js # Queue API operations
|
||||
├── queue-renderer.js # Render queue items (pending, active, etc.)
|
||||
└── progress-handler.js # Real-time progress updates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Extract Shared JavaScript Utilities
|
||||
|
||||
**Objective**: Create reusable utility modules used by both index and queue pages.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Create constants.js**:
|
||||
|
||||
- Extract API endpoint URLs
|
||||
- Extract localStorage keys
|
||||
- Extract any magic strings or numbers
|
||||
|
||||
2. **Create auth.js**:
|
||||
|
||||
- Extract `checkAuth()` function
|
||||
- Extract `logout()` function
|
||||
- Extract `getAuthHeaders()` or token retrieval logic
|
||||
- Extract token storage/retrieval from localStorage
|
||||
|
||||
3. **Create api-client.js**:
|
||||
|
||||
- Extract `fetchWithAuth()` wrapper function
|
||||
- Handle automatic token injection
|
||||
- Handle 401 redirect to login
|
||||
- Handle common error responses
|
||||
|
||||
4. **Create websocket-client.js**:
|
||||
|
||||
- Extract WebSocket connection setup
|
||||
- Extract message handling dispatcher
|
||||
- Extract reconnection logic
|
||||
- Extract connection state management
|
||||
|
||||
5. **Create theme.js**:
|
||||
|
||||
- Extract `initTheme()` function
|
||||
- Extract `toggleTheme()` function
|
||||
- Extract `setTheme()` function
|
||||
- Extract theme persistence to localStorage
|
||||
|
||||
6. **Create ui-utils.js**:
|
||||
- Extract `showToast()` function
|
||||
- Extract `showLoadingOverlay()` / `hideLoadingOverlay()`
|
||||
- Extract `formatBytes()` function
|
||||
- Extract `formatDuration()` function
|
||||
- Extract `formatDate()` function
|
||||
- Extract any other shared UI helpers
|
||||
|
||||
**Pattern to Use (IIFE with Global Namespace)**:
|
||||
|
||||
```javascript
|
||||
// Example: shared/auth.js
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.Auth = (function () {
|
||||
"use strict";
|
||||
|
||||
const TOKEN_KEY = "auth_token";
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
function setToken(token) {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
function removeToken() {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
function getAuthHeaders() {
|
||||
const token = getToken();
|
||||
return token ? { Authorization: "Bearer " + token } : {};
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
function logout() {
|
||||
removeToken();
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
getToken: getToken,
|
||||
setToken: setToken,
|
||||
getAuthHeaders: getAuthHeaders,
|
||||
checkAuth: checkAuth,
|
||||
logout: logout,
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Split app.js into Index Page Modules
|
||||
|
||||
**Objective**: Break down `app.js` into focused modules for the index/library page.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Create series-manager.js**:
|
||||
|
||||
- Extract series loading from API
|
||||
- Extract series filtering logic
|
||||
- Extract series rendering/DOM updates
|
||||
- Extract series card click handlers
|
||||
|
||||
2. **Create search.js**:
|
||||
|
||||
- Extract search input handling
|
||||
- Extract search API calls
|
||||
- Extract search results rendering
|
||||
- Extract search result selection
|
||||
|
||||
3. **Create scan-manager.js**:
|
||||
|
||||
- Extract scan initiation logic
|
||||
- Extract scan progress overlay
|
||||
- Extract scan progress updates (WebSocket)
|
||||
- Extract scan completion handling
|
||||
|
||||
4. **Create config-manager.js**:
|
||||
|
||||
- Extract config modal open/close
|
||||
- Extract config loading from API
|
||||
- Extract config form handling
|
||||
- Extract config save logic
|
||||
- Extract scheduler configuration
|
||||
- Extract backup management
|
||||
|
||||
5. **Create selection.js**:
|
||||
|
||||
- Extract episode selection logic
|
||||
- Extract "select all" functionality
|
||||
- Extract selection state management
|
||||
- Extract "add to queue" from selection
|
||||
|
||||
6. **Update main app.js**:
|
||||
- Import all modules via script tags
|
||||
- Initialize all modules on DOMContentLoaded
|
||||
- Wire up event listeners to module functions
|
||||
- Keep this file as thin as possible (orchestration only)
|
||||
|
||||
**Example main app.js structure**:
|
||||
|
||||
```javascript
|
||||
// filepath: src/server/web/static/js/app.js
|
||||
document.addEventListener("DOMContentLoaded", async function () {
|
||||
"use strict";
|
||||
|
||||
// Initialize shared modules
|
||||
AniWorld.Theme.init();
|
||||
|
||||
// Check authentication
|
||||
const isAuth = await AniWorld.Auth.checkAuth();
|
||||
if (!isAuth) return;
|
||||
|
||||
// Initialize page-specific modules
|
||||
AniWorld.SeriesManager.init();
|
||||
AniWorld.Search.init();
|
||||
AniWorld.ScanManager.init();
|
||||
AniWorld.ConfigManager.init();
|
||||
AniWorld.Selection.init();
|
||||
|
||||
// Initialize WebSocket for real-time updates
|
||||
AniWorld.WebSocketClient.init();
|
||||
|
||||
// Load initial data
|
||||
AniWorld.SeriesManager.loadSeries();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Split queue.js into Queue Page Modules
|
||||
|
||||
**Objective**: Break down `queue.js` into focused modules for the queue page.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Create queue-api.js**:
|
||||
|
||||
- Extract `loadQueueStatus()` API call
|
||||
- Extract `startDownload()` API call
|
||||
- Extract `stopDownload()` API call
|
||||
- Extract `removeFromQueue()` API call
|
||||
- Extract `clearCompleted()` API call
|
||||
- Extract `clearFailed()` API call
|
||||
- Extract `retryFailed()` API call
|
||||
|
||||
2. **Create queue-renderer.js**:
|
||||
|
||||
- Extract `renderActiveDownload()` function
|
||||
- Extract `renderPendingQueue()` function
|
||||
- Extract `renderCompletedList()` function
|
||||
- Extract `renderFailedList()` function
|
||||
- Extract `updateQueueCounts()` function
|
||||
- Extract queue item template generation
|
||||
|
||||
3. **Create progress-handler.js**:
|
||||
|
||||
- Extract WebSocket message handling for queue
|
||||
- Extract progress bar updates
|
||||
- Extract status text updates
|
||||
- Extract ETA calculations
|
||||
- Extract speed display formatting
|
||||
|
||||
4. **Update main queue.js**:
|
||||
- Import all modules via script tags
|
||||
- Initialize all modules on DOMContentLoaded
|
||||
- Wire up button click handlers to API functions
|
||||
- Set up WebSocket handlers for progress
|
||||
- Keep this file as thin as possible
|
||||
|
||||
**Example main queue.js structure**:
|
||||
|
||||
```javascript
|
||||
// filepath: src/server/web/static/js/queue.js
|
||||
document.addEventListener("DOMContentLoaded", async function () {
|
||||
"use strict";
|
||||
|
||||
// Initialize shared modules
|
||||
AniWorld.Theme.init();
|
||||
|
||||
// Check authentication
|
||||
const isAuth = await AniWorld.Auth.checkAuth();
|
||||
if (!isAuth) return;
|
||||
|
||||
// Initialize queue modules
|
||||
AniWorld.QueueApi.init();
|
||||
AniWorld.QueueRenderer.init();
|
||||
AniWorld.ProgressHandler.init();
|
||||
|
||||
// Initialize WebSocket with queue-specific handlers
|
||||
AniWorld.WebSocketClient.init({
|
||||
onProgress: AniWorld.ProgressHandler.handleProgress,
|
||||
onQueueUpdate: AniWorld.QueueRenderer.refresh,
|
||||
});
|
||||
|
||||
// Load initial queue status
|
||||
await AniWorld.QueueApi.loadStatus();
|
||||
AniWorld.QueueRenderer.refresh();
|
||||
|
||||
// Wire up UI buttons
|
||||
document
|
||||
.getElementById("start-btn")
|
||||
?.addEventListener("click", AniWorld.QueueApi.startDownload);
|
||||
document
|
||||
.getElementById("stop-btn")
|
||||
?.addEventListener("click", AniWorld.QueueApi.stopDownload);
|
||||
document
|
||||
.getElementById("clear-completed-btn")
|
||||
?.addEventListener("click", AniWorld.QueueApi.clearCompleted);
|
||||
document
|
||||
.getElementById("clear-failed-btn")
|
||||
?.addEventListener("click", AniWorld.QueueApi.clearFailed);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Update HTML Templates
|
||||
|
||||
**Objective**: Update templates to load the new modular JavaScript files.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Update index.html**:
|
||||
|
||||
- Add script tags for shared modules (in order)
|
||||
- Add script tags for index-specific modules (in order)
|
||||
- Keep main app.js as the last script
|
||||
- Ensure correct load order (dependencies first)
|
||||
|
||||
```html
|
||||
<!-- Shared Modules -->
|
||||
<script src="/static/js/shared/constants.js"></script>
|
||||
<script src="/static/js/shared/auth.js"></script>
|
||||
<script src="/static/js/shared/api-client.js"></script>
|
||||
<script src="/static/js/shared/websocket-client.js"></script>
|
||||
<script src="/static/js/shared/theme.js"></script>
|
||||
<script src="/static/js/shared/ui-utils.js"></script>
|
||||
|
||||
<!-- Index Page Modules -->
|
||||
<script src="/static/js/index/series-manager.js"></script>
|
||||
<script src="/static/js/index/search.js"></script>
|
||||
<script src="/static/js/index/scan-manager.js"></script>
|
||||
<script src="/static/js/index/config-manager.js"></script>
|
||||
<script src="/static/js/index/selection.js"></script>
|
||||
|
||||
<!-- Main Entry Point -->
|
||||
<script src="/static/js/app.js"></script>
|
||||
```
|
||||
|
||||
2. **Update queue.html**:
|
||||
|
||||
- Add script tags for shared modules (in order)
|
||||
- Add script tags for queue-specific modules (in order)
|
||||
- Keep main queue.js as the last script
|
||||
|
||||
```html
|
||||
<!-- Shared Modules -->
|
||||
<script src="/static/js/shared/constants.js"></script>
|
||||
<script src="/static/js/shared/auth.js"></script>
|
||||
<script src="/static/js/shared/api-client.js"></script>
|
||||
<script src="/static/js/shared/websocket-client.js"></script>
|
||||
<script src="/static/js/shared/theme.js"></script>
|
||||
<script src="/static/js/shared/ui-utils.js"></script>
|
||||
|
||||
<!-- Queue Page Modules -->
|
||||
<script src="/static/js/queue/queue-api.js"></script>
|
||||
<script src="/static/js/queue/queue-renderer.js"></script>
|
||||
<script src="/static/js/queue/progress-handler.js"></script>
|
||||
|
||||
<!-- Main Entry Point -->
|
||||
<script src="/static/js/queue.js"></script>
|
||||
```
|
||||
|
||||
3. **Update login.html** (if applicable):
|
||||
- Only include shared modules needed for login
|
||||
- Likely just theme.js and minimal utilities
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Verification and Testing
|
||||
|
||||
**Objective**: Ensure all functionality works after refactoring.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Start the server**:
|
||||
|
||||
```bash
|
||||
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
|
||||
```
|
||||
|
||||
2. **Test Login Page**:
|
||||
|
||||
- [ ] Page loads with correct styling
|
||||
- [ ] Dark/light mode toggle works
|
||||
- [ ] Login form submits correctly
|
||||
- [ ] Error messages display correctly
|
||||
- [ ] Successful login redirects to index
|
||||
|
||||
3. **Test Index Page**:
|
||||
|
||||
- [ ] Page loads with correct styling
|
||||
- [ ] Series list loads and displays
|
||||
- [ ] Series filtering works
|
||||
- [ ] Search functionality works
|
||||
- [ ] Series selection works
|
||||
- [ ] Episode selection works
|
||||
- [ ] Add to queue works
|
||||
- [ ] Scan library works
|
||||
- [ ] Scan progress displays
|
||||
- [ ] Config modal opens/closes
|
||||
- [ ] Config saves correctly
|
||||
- [ ] Dark/light mode toggle works
|
||||
- [ ] Logout works
|
||||
- [ ] WebSocket connection established
|
||||
|
||||
4. **Test Queue Page**:
|
||||
|
||||
- [ ] Page loads with correct styling
|
||||
- [ ] Queue status loads
|
||||
- [ ] Pending items display
|
||||
- [ ] Active download displays
|
||||
- [ ] Completed items display
|
||||
- [ ] Failed items display
|
||||
- [ ] Start download works
|
||||
- [ ] Stop download works
|
||||
- [ ] Remove from queue works
|
||||
- [ ] Clear completed works
|
||||
- [ ] Clear failed works
|
||||
- [ ] Retry failed works
|
||||
- [ ] Progress updates in real-time
|
||||
- [ ] Dark/light mode toggle works
|
||||
- [ ] WebSocket connection established
|
||||
|
||||
5. **Test Responsive Design**:
|
||||
|
||||
- [ ] All pages work on mobile viewport
|
||||
- [ ] All pages work on tablet viewport
|
||||
- [ ] All pages work on desktop viewport
|
||||
|
||||
6. **Browser Console Check**:
|
||||
- [ ] No JavaScript errors in console
|
||||
- [ ] No 404 errors for static files
|
||||
- [ ] No CSS loading errors
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Cleanup and Documentation
|
||||
|
||||
**Objective**: Finalize the refactoring with cleanup and documentation.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Remove backup files** (if any were created)
|
||||
|
||||
2. **Verify file sizes**:
|
||||
|
||||
- No file should exceed 500 lines
|
||||
- If any file exceeds, split further
|
||||
|
||||
3. **Add file headers**:
|
||||
|
||||
- Add comment header to each new file explaining its purpose
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* AniWorld - Series Manager Module
|
||||
*
|
||||
* Handles loading, filtering, and rendering of anime series
|
||||
* on the index/library page.
|
||||
*
|
||||
* Dependencies: auth.js, api-client.js, ui-utils.js
|
||||
*/
|
||||
```
|
||||
|
||||
```css
|
||||
/**
|
||||
* AniWorld - Button Styles
|
||||
*
|
||||
* All button-related styles including variants,
|
||||
* states, and sizes.
|
||||
*/
|
||||
```
|
||||
|
||||
4. **Update infrastructure.md** (if exists):
|
||||
|
||||
- Document new file structure
|
||||
- Document module dependencies
|
||||
|
||||
5. **Commit changes**:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "refactor: split CSS and JS into modular files (SRP)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Summary of New Files
|
||||
|
||||
**CSS Files (14 files)**:
|
||||
|
||||
- `css/styles.css` (entry point with imports)
|
||||
- `css/base/variables.css`
|
||||
- `css/base/reset.css`
|
||||
- `css/base/typography.css`
|
||||
- `css/components/buttons.css`
|
||||
- `css/components/cards.css`
|
||||
- `css/components/forms.css`
|
||||
- `css/components/modals.css`
|
||||
- `css/components/navigation.css`
|
||||
- `css/components/progress.css`
|
||||
- `css/components/notifications.css`
|
||||
- `css/components/tables.css`
|
||||
- `css/pages/login.css`
|
||||
- `css/pages/index.css`
|
||||
- `css/pages/queue.css`
|
||||
- `css/utilities/animations.css`
|
||||
- `css/utilities/responsive.css`
|
||||
- `css/utilities/helpers.css`
|
||||
|
||||
**JavaScript Files (15 files)**:
|
||||
|
||||
- `js/app.js` (entry point for index)
|
||||
- `js/queue.js` (entry point for queue)
|
||||
- `js/shared/constants.js`
|
||||
- `js/shared/auth.js`
|
||||
- `js/shared/api-client.js`
|
||||
- `js/shared/websocket-client.js`
|
||||
- `js/shared/theme.js`
|
||||
- `js/shared/ui-utils.js`
|
||||
- `js/index/series-manager.js`
|
||||
- `js/index/search.js`
|
||||
- `js/index/scan-manager.js`
|
||||
- `js/index/config-manager.js`
|
||||
- `js/index/selection.js`
|
||||
- `js/queue/queue-api.js`
|
||||
- `js/queue/queue-renderer.js`
|
||||
- `js/queue/progress-handler.js`
|
||||
|
||||
---
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. **IIFE Pattern**: Use the IIFE (Immediately Invoked Function Expression) pattern with a global namespace (`AniWorld`) for browser compatibility without requiring a build step.
|
||||
|
||||
2. **No Build Tools Required**: This approach uses native CSS `@import` and multiple `<script>` tags, avoiding the need for bundlers like Webpack or Vite.
|
||||
|
||||
3. **Load Order Matters**: Scripts must be loaded in dependency order. Shared modules first, then page-specific modules, then the main entry point.
|
||||
|
||||
4. **Backward Compatibility**: All existing HTML element IDs and class names must be preserved. Only the JavaScript and CSS organization changes, not the API.
|
||||
|
||||
5. **Incremental Approach**: Complete one task fully before moving to the next. Verify functionality after each major step.
|
||||
|
||||
6. **Rollback Plan**: Keep the original files until all verification is complete. Only delete originals after confirming everything works.
|
||||
|
||||
@ -199,6 +199,24 @@ class SeriesApp:
|
||||
"""Set scan_status event handler."""
|
||||
self._events.scan_status = value
|
||||
|
||||
def request_download_cancel(self) -> None:
|
||||
"""Request cancellation of any ongoing download.
|
||||
|
||||
This method signals the download provider to stop any active
|
||||
downloads. The actual cancellation happens asynchronously in
|
||||
the progress hook of the downloader.
|
||||
"""
|
||||
logger.info("Requesting download cancellation")
|
||||
self.loader.request_cancel()
|
||||
|
||||
def reset_download_cancel(self) -> None:
|
||||
"""Reset the download cancellation flag.
|
||||
|
||||
Should be called before starting a new download to ensure
|
||||
it's not immediately cancelled.
|
||||
"""
|
||||
self.loader.reset_cancel()
|
||||
|
||||
def load_series_from_list(self, series: list) -> None:
|
||||
"""
|
||||
Load series into the in-memory list.
|
||||
@ -286,6 +304,9 @@ class SeriesApp:
|
||||
lookups. The 'serie_folder' parameter is only used for
|
||||
filesystem operations.
|
||||
"""
|
||||
# Reset cancel flag before starting new download
|
||||
self.reset_download_cancel()
|
||||
|
||||
logger.info(
|
||||
"Starting download: %s (key: %s) S%02dE%02d",
|
||||
serie_folder,
|
||||
|
||||
@ -4,6 +4,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
@ -70,6 +71,9 @@ class AniworldLoader(Loader):
|
||||
}
|
||||
self.ANIWORLD_TO = "https://aniworld.to"
|
||||
self.session = requests.Session()
|
||||
|
||||
# Cancellation flag for graceful shutdown
|
||||
self._cancel_flag = threading.Event()
|
||||
|
||||
# Configure retries with backoff
|
||||
retries = Retry(
|
||||
@ -198,6 +202,30 @@ class AniworldLoader(Loader):
|
||||
logging.debug(f"Available languages for S{season:02}E{episode:03}: {languages}, requested: {language_code}, available: {is_available}")
|
||||
return is_available
|
||||
|
||||
def request_cancel(self) -> None:
|
||||
"""Request cancellation of any ongoing download.
|
||||
|
||||
Sets the internal cancellation flag. Downloads will check this
|
||||
flag periodically and abort if set.
|
||||
"""
|
||||
logging.info("Download cancellation requested")
|
||||
self._cancel_flag.set()
|
||||
|
||||
def reset_cancel(self) -> None:
|
||||
"""Reset the cancellation flag.
|
||||
|
||||
Should be called before starting a new download.
|
||||
"""
|
||||
self._cancel_flag.clear()
|
||||
|
||||
def is_cancelled(self) -> bool:
|
||||
"""Check if cancellation has been requested.
|
||||
|
||||
Returns:
|
||||
bool: True if cancellation was requested
|
||||
"""
|
||||
return self._cancel_flag.is_set()
|
||||
|
||||
def download(
|
||||
self,
|
||||
base_directory: str,
|
||||
@ -223,7 +251,15 @@ class AniworldLoader(Loader):
|
||||
|
||||
Returns:
|
||||
bool: True if download succeeded, False otherwise
|
||||
|
||||
Raises:
|
||||
asyncio.CancelledError: If download was cancelled via request_cancel()
|
||||
"""
|
||||
# Check cancellation before starting
|
||||
if self.is_cancelled():
|
||||
logging.info("Download cancelled before starting")
|
||||
raise InterruptedError("Download cancelled")
|
||||
|
||||
logging.info(
|
||||
f"Starting download for S{season:02}E{episode:03} "
|
||||
f"({key}) in {language}"
|
||||
@ -261,11 +297,26 @@ class AniworldLoader(Loader):
|
||||
logging.debug(f"Temporary path: {temp_path}")
|
||||
|
||||
for provider in self.SUPPORTED_PROVIDERS:
|
||||
# Check cancellation before each provider attempt
|
||||
if self.is_cancelled():
|
||||
logging.info("Download cancelled during provider selection")
|
||||
raise InterruptedError("Download cancelled")
|
||||
|
||||
logging.debug(f"Attempting download with provider: {provider}")
|
||||
link, header = self._get_direct_link_from_provider(
|
||||
season, episode, key, language
|
||||
)
|
||||
logging.debug("Direct link obtained from provider")
|
||||
|
||||
# Create a cancellation-aware progress hook
|
||||
cancel_flag = self._cancel_flag
|
||||
|
||||
def cancellation_check_hook(d):
|
||||
"""Progress hook that checks for cancellation."""
|
||||
if cancel_flag.is_set():
|
||||
logging.info("Cancellation detected in progress hook")
|
||||
raise InterruptedError("Download cancelled")
|
||||
|
||||
ydl_opts = {
|
||||
'fragment_retries': float('inf'),
|
||||
'outtmpl': temp_path,
|
||||
@ -273,14 +324,20 @@ class AniworldLoader(Loader):
|
||||
'no_warnings': True,
|
||||
'progress_with_newline': False,
|
||||
'nocheckcertificate': True,
|
||||
# Add cancellation check as a progress hook
|
||||
'progress_hooks': [cancellation_check_hook],
|
||||
}
|
||||
|
||||
if header:
|
||||
ydl_opts['http_headers'] = header
|
||||
logging.debug("Using custom headers for download")
|
||||
if progress_callback:
|
||||
# Wrap the callback to add logging
|
||||
# Wrap the callback to add logging and keep cancellation check
|
||||
def logged_progress_callback(d):
|
||||
# Check cancellation first
|
||||
if cancel_flag.is_set():
|
||||
logging.info("Cancellation detected in progress callback")
|
||||
raise InterruptedError("Download cancelled")
|
||||
logging.debug(
|
||||
f"YT-DLP progress: status={d.get('status')}, "
|
||||
f"downloaded={d.get('downloaded_bytes')}, "
|
||||
@ -305,6 +362,14 @@ class AniworldLoader(Loader):
|
||||
f"filesize={info.get('filesize')}"
|
||||
)
|
||||
|
||||
# Check cancellation after download completes
|
||||
if self.is_cancelled():
|
||||
logging.info("Download cancelled after completion")
|
||||
# Clean up temp file if exists
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
raise InterruptedError("Download cancelled")
|
||||
|
||||
if os.path.exists(temp_path):
|
||||
logging.debug("Moving file from temp to final destination")
|
||||
shutil.copy(temp_path, output_path)
|
||||
@ -320,6 +385,16 @@ class AniworldLoader(Loader):
|
||||
)
|
||||
self.clear_cache()
|
||||
return False
|
||||
except InterruptedError:
|
||||
# Re-raise cancellation errors
|
||||
logging.info("Download interrupted, propagating cancellation")
|
||||
# Clean up temp file if exists
|
||||
if os.path.exists(temp_path):
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
except BrokenPipeError as e:
|
||||
logging.error(
|
||||
f"Broken pipe error with provider {provider}: {e}. "
|
||||
|
||||
@ -5,6 +5,30 @@ from typing import Any, Callable, Dict, List, Optional
|
||||
class Loader(ABC):
|
||||
"""Abstract base class for anime data loaders/providers."""
|
||||
|
||||
@abstractmethod
|
||||
def request_cancel(self) -> None:
|
||||
"""Request cancellation of any ongoing download.
|
||||
|
||||
Sets an internal flag that downloads should check periodically
|
||||
and abort if set. This enables graceful shutdown.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def reset_cancel(self) -> None:
|
||||
"""Reset the cancellation flag.
|
||||
|
||||
Should be called before starting a new download to ensure
|
||||
it's not immediately cancelled.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def is_cancelled(self) -> bool:
|
||||
"""Check if cancellation has been requested.
|
||||
|
||||
Returns:
|
||||
bool: True if cancellation was requested
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def search(self, word: str) -> List[Dict[str, Any]]:
|
||||
"""Search for anime series by name.
|
||||
|
||||
@ -72,6 +72,21 @@ class AnimeService:
|
||||
logger.exception("Failed to subscribe to SeriesApp events")
|
||||
raise AnimeServiceError("Initialization failed") from e
|
||||
|
||||
def request_download_cancel(self) -> None:
|
||||
"""Request cancellation of any ongoing download.
|
||||
|
||||
This method signals the underlying download provider to stop
|
||||
any active downloads. The cancellation happens asynchronously
|
||||
via progress hooks in the downloader.
|
||||
|
||||
Should be called during shutdown to stop in-progress downloads.
|
||||
"""
|
||||
logger.info("Requesting download cancellation via AnimeService")
|
||||
try:
|
||||
self._app.request_download_cancel()
|
||||
except Exception as e:
|
||||
logger.warning("Failed to request download cancellation: %s", e)
|
||||
|
||||
def _on_download_status(self, args) -> None:
|
||||
"""Handle download status events from SeriesApp.
|
||||
|
||||
|
||||
@ -1012,6 +1012,13 @@ class DownloadService:
|
||||
self._is_shutting_down = True
|
||||
self._is_stopped = True
|
||||
|
||||
# Request cancellation from AnimeService (signals the download thread)
|
||||
try:
|
||||
self._anime_service.request_download_cancel()
|
||||
logger.info("Requested download cancellation from AnimeService")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to request download cancellation: %s", e)
|
||||
|
||||
# Persist active download back to pending state if one exists
|
||||
if self._active_download:
|
||||
logger.info(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user