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:
Lukas 2025-12-27 19:31:57 +01:00
parent 778d16b21a
commit 08f816a954
14 changed files with 145 additions and 900 deletions

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -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,

View File

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

View File

@ -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.

View File

@ -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.

View File

@ -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(