mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-24 18:31:43 +01:00
Fix: Add SABnzbd cleanup, file directory notice, audiobook button (#444)
- Add SABnzbd archive cleanup upon download completion - Added frontend audiobook library URL button - Clearer errors for misconfigured destination paths - Test clients.yml added
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# Test stack for download client development
|
||||
# Includes shelfmark + all download clients on same network with shared volumes
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.test-clients.yml up -d
|
||||
# # Access shelfmark at http://localhost:8084
|
||||
# # Configure clients in Settings > Prowlarr > Download Clients
|
||||
#
|
||||
# Web UIs:
|
||||
# - shelfmark: http://localhost:8084
|
||||
# - Prowlarr: http://localhost:9696 (no auth by default)
|
||||
# - qBittorrent: http://localhost:8080 (check container logs for temp password)
|
||||
# - Transmission: http://localhost:9091 (admin / admin)
|
||||
# - Deluge: http://localhost:8112 (admin / deluge)
|
||||
# - NZBGet: http://localhost:6789 (nzbget / tegbzn6789)
|
||||
# - SABnzbd: http://localhost:8085 (complete setup wizard for API key)
|
||||
#
|
||||
|
||||
|
||||
services:
|
||||
shelfmark:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: shelfmark
|
||||
container_name: test-shelfmark
|
||||
cap_add:
|
||||
- SYS_PTRACE
|
||||
environment:
|
||||
TZ: UTC
|
||||
DEBUG: "true"
|
||||
# All client configuration is done via Settings UI
|
||||
# Use Docker service names for URLs:
|
||||
# - qBittorrent: http://qbittorrent:8080
|
||||
# - Transmission: http://transmission:9091
|
||||
# - Deluge host: deluge (port 58846)
|
||||
# - NZBGet: http://nzbget:6789
|
||||
# - SABnzbd: http://sabnzbd:8080
|
||||
ports:
|
||||
- "8084:8084"
|
||||
volumes:
|
||||
# Config and state
|
||||
- ./.local/test-clients/shelfmark/config:/config
|
||||
- ./.local/test-clients/shelfmark/log:/var/log/shelfmark
|
||||
# Book destination directory (where completed books go)
|
||||
- ./.local/test-clients/books:/books
|
||||
# Staging directory
|
||||
- ./.local/test-clients/tmp:/tmp/shelfmark
|
||||
# CRITICAL: Mount client download directories so shelfmark can access completed files
|
||||
- ./.local/test-clients/downloads:/downloads
|
||||
# Mount source code for hot-reload (no rebuild needed for Python changes)
|
||||
- ./shelfmark:/app/shelfmark:ro
|
||||
# Mount tests for running pytest in container
|
||||
- ./tests:/app/tests:ro
|
||||
- ./pyproject.toml:/app/pyproject.toml:ro
|
||||
# Mount client configs for integration tests to read credentials
|
||||
- ./.local/test-clients/qbittorrent/config:/qbittorrent-config:ro
|
||||
- ./.local/test-clients/sabnzbd/config:/sabnzbd-config:ro
|
||||
depends_on:
|
||||
- nzbget
|
||||
- sabnzbd
|
||||
- qbittorrent
|
||||
- transmission
|
||||
- deluge
|
||||
restart: unless-stopped
|
||||
|
||||
prowlarr:
|
||||
image: lscr.io/linuxserver/prowlarr:latest
|
||||
container_name: test-prowlarr
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=UTC
|
||||
volumes:
|
||||
- ./.local/test-clients/prowlarr/config:/config
|
||||
ports:
|
||||
- "9696:9696"
|
||||
restart: unless-stopped
|
||||
|
||||
nzbget:
|
||||
image: lscr.io/linuxserver/nzbget:latest
|
||||
container_name: test-nzbget
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=UTC
|
||||
volumes:
|
||||
- ./.local/test-clients/nzbget/config:/config
|
||||
- ./.local/test-clients/downloads:/downloads
|
||||
- ./.local/test-clients/nzbget/custom-cont-init.d:/custom-cont-init.d:ro
|
||||
ports:
|
||||
- "6789:6789" # Web UI / JSON-RPC
|
||||
restart: unless-stopped
|
||||
|
||||
sabnzbd:
|
||||
image: lscr.io/linuxserver/sabnzbd:latest
|
||||
container_name: test-sabnzbd
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=UTC
|
||||
volumes:
|
||||
- ./.local/test-clients/sabnzbd/config:/config
|
||||
- ./.local/test-clients/downloads:/downloads
|
||||
ports:
|
||||
- "8085:8080" # Web UI (external:internal)
|
||||
restart: unless-stopped
|
||||
|
||||
qbittorrent:
|
||||
image: lscr.io/linuxserver/qbittorrent:latest
|
||||
container_name: test-qbittorrent
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=UTC
|
||||
- WEBUI_PORT=8080
|
||||
volumes:
|
||||
- ./.local/test-clients/qbittorrent/config:/config
|
||||
- ./.local/test-clients/downloads:/downloads
|
||||
- ./.local/test-clients/qbittorrent/custom-cont-init.d:/custom-cont-init.d:ro
|
||||
ports:
|
||||
- "8080:8080" # Web UI / API
|
||||
- "6882:6881"
|
||||
- "6882:6881/udp"
|
||||
restart: unless-stopped
|
||||
|
||||
transmission:
|
||||
image: lscr.io/linuxserver/transmission:latest
|
||||
container_name: test-transmission
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=UTC
|
||||
- USER=admin
|
||||
- PASS=admin
|
||||
volumes:
|
||||
- ./.local/test-clients/transmission/config:/config
|
||||
- ./.local/test-clients/downloads:/downloads
|
||||
ports:
|
||||
- "9091:9091" # Web UI / RPC
|
||||
- "51413:51413"
|
||||
- "51413:51413/udp"
|
||||
restart: unless-stopped
|
||||
|
||||
deluge:
|
||||
image: lscr.io/linuxserver/deluge:latest
|
||||
container_name: test-deluge
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=UTC
|
||||
- DELUGE_LOGLEVEL=error
|
||||
volumes:
|
||||
- ./.local/test-clients/deluge/config:/config
|
||||
- ./.local/test-clients/downloads:/downloads
|
||||
ports:
|
||||
- "8112:8112" # Web UI
|
||||
- "58846:58846" # Daemon RPC
|
||||
- "6881:6881"
|
||||
- "6881:6881/udp"
|
||||
restart: unless-stopped
|
||||
@@ -268,10 +268,17 @@ def general_settings():
|
||||
return [
|
||||
TextField(
|
||||
key="CALIBRE_WEB_URL",
|
||||
label="Book Management App URL",
|
||||
description="Adds a navigation button to your book manager instance (Calibre-Web Automated, Booklore, etc).",
|
||||
label="Library URL",
|
||||
description="Adds a navigation button to your book library (Calibre-Web Automated, Booklore, etc).",
|
||||
placeholder="http://calibre-web:8083",
|
||||
),
|
||||
TextField(
|
||||
key="AUDIOBOOK_LIBRARY_URL",
|
||||
label="Audiobook Library URL",
|
||||
description="Adds a separate navigation button for your audiobook library (Audiobookshelf, Plex, etc). When both URLs are set, icons are shown instead of text.",
|
||||
placeholder="http://audiobookshelf:8080",
|
||||
env_supported=False,
|
||||
),
|
||||
HeadingField(
|
||||
key="search_defaults_heading",
|
||||
title="Default Search Filters",
|
||||
|
||||
@@ -552,7 +552,14 @@ def _download_task(task_id: str, cancel_flag: Event) -> Optional[str]:
|
||||
task = book_queue.get_task(task_id)
|
||||
if task:
|
||||
book_queue.update_status(task_id, QueueStatus.ERROR)
|
||||
book_queue.update_status_message(task_id, f"Download failed: {type(e).__name__}")
|
||||
# Check for known misconfiguration from earlier versions
|
||||
if isinstance(e, PermissionError) and "/cwa-book-ingest" in str(e):
|
||||
book_queue.update_status_message(
|
||||
task_id,
|
||||
"Destination misconfigured. Go to Settings → Downloads to update."
|
||||
)
|
||||
else:
|
||||
book_queue.update_status_message(task_id, f"Download failed: {type(e).__name__}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -496,6 +496,7 @@ def api_config() -> Union[Response, Tuple[Response, int]]:
|
||||
|
||||
config = {
|
||||
"calibre_web_url": app_config.get("CALIBRE_WEB_URL", ""),
|
||||
"audiobook_library_url": app_config.get("AUDIOBOOK_LIBRARY_URL", ""),
|
||||
"debug": app_config.get("DEBUG", False),
|
||||
"build_version": BUILD_VERSION,
|
||||
"release_version": RELEASE_VERSION,
|
||||
|
||||
@@ -280,13 +280,14 @@ class SABnzbdClient(DownloadClient):
|
||||
logger.error(f"SABnzbd get_status failed ({error_type}): {e}")
|
||||
return DownloadStatus.error(f"{error_type}: {e}")
|
||||
|
||||
def remove(self, download_id: str, delete_files: bool = False) -> bool:
|
||||
def remove(self, download_id: str, delete_files: bool = False, archive: bool = True) -> bool:
|
||||
"""
|
||||
Remove a download from SABnzbd.
|
||||
|
||||
Args:
|
||||
download_id: SABnzbd nzo_id
|
||||
delete_files: Whether to delete the files
|
||||
archive: If True, move to archive instead of permanent delete (history only)
|
||||
|
||||
Returns:
|
||||
True if successful.
|
||||
@@ -313,11 +314,13 @@ class SABnzbdClient(DownloadClient):
|
||||
"name": "delete",
|
||||
"value": download_id,
|
||||
"del_files": 1 if delete_files else 0,
|
||||
"archive": 1 if archive else 0,
|
||||
},
|
||||
)
|
||||
|
||||
if result.get("status"):
|
||||
logger.info(f"Removed NZB from SABnzbd history: {download_id}")
|
||||
action = "archived" if archive else "removed"
|
||||
logger.info(f"NZB {action} from SABnzbd history: {download_id}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -44,6 +44,14 @@ class ProwlarrHandler(DownloadHandler):
|
||||
audiobook_key = audiobook_keys.get(client.name)
|
||||
return config.get(audiobook_key, "") or None if audiobook_key else None
|
||||
|
||||
def _cleanup_client_history(self, client, download_id: str) -> None:
|
||||
"""Remove completed download from client history if configured."""
|
||||
if client.name == "sabnzbd" and config.get("SABNZBD_REMOVE_COMPLETED", True):
|
||||
try:
|
||||
client.remove(download_id, delete_files=True, archive=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove from SABnzbd history: {e}")
|
||||
|
||||
def _build_progress_message(self, status) -> str:
|
||||
"""Build a progress message from download status."""
|
||||
msg = f"{status.progress:.0f}%"
|
||||
@@ -127,6 +135,7 @@ class ProwlarrHandler(DownloadHandler):
|
||||
|
||||
if result:
|
||||
remove_release(task.task_id)
|
||||
self._cleanup_client_history(client, download_id)
|
||||
return result
|
||||
|
||||
# Existing but still downloading - join the progress polling
|
||||
@@ -256,9 +265,10 @@ class ProwlarrHandler(DownloadHandler):
|
||||
status_callback=status_callback,
|
||||
)
|
||||
|
||||
# Clean up cache on success
|
||||
# Clean up on success
|
||||
if result:
|
||||
remove_release(task.task_id)
|
||||
self._cleanup_client_history(client, download_id)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -613,6 +613,13 @@ def prowlarr_clients_settings():
|
||||
default="",
|
||||
show_when={"field": "PROWLARR_USENET_CLIENT", "value": "sabnzbd"},
|
||||
),
|
||||
CheckboxField(
|
||||
key="SABNZBD_REMOVE_COMPLETED",
|
||||
label="Remove completed downloads from history",
|
||||
default=True,
|
||||
description="Remove downloads from SABnzbd history after successful import (archives them)",
|
||||
show_when={"field": "PROWLARR_USENET_CLIENT", "value": "sabnzbd"},
|
||||
),
|
||||
|
||||
# Note: Usenet client download path must be mounted identically in both containers.
|
||||
SelectField(
|
||||
|
||||
@@ -541,6 +541,7 @@ function App() {
|
||||
<SearchModeProvider searchMode={searchMode}>
|
||||
<Header
|
||||
calibreWebUrl={config?.calibre_web_url || ''}
|
||||
audiobookLibraryUrl={config?.audiobook_library_url || ''}
|
||||
debug={config?.debug || false}
|
||||
logoUrl="/logo.png"
|
||||
showSearch={!isInitialState}
|
||||
|
||||
@@ -14,6 +14,7 @@ interface StatusCounts {
|
||||
|
||||
interface HeaderProps {
|
||||
calibreWebUrl?: string;
|
||||
audiobookLibraryUrl?: string;
|
||||
debug?: boolean;
|
||||
logoUrl?: string;
|
||||
showSearch?: boolean;
|
||||
@@ -37,6 +38,7 @@ interface HeaderProps {
|
||||
|
||||
export const Header = forwardRef<HeaderHandle, HeaderProps>(({
|
||||
calibreWebUrl,
|
||||
audiobookLibraryUrl,
|
||||
debug,
|
||||
logoUrl,
|
||||
showSearch = false,
|
||||
@@ -157,23 +159,43 @@ export const Header = forwardRef<HeaderHandle, HeaderProps>(({
|
||||
onSearchChange?.(value);
|
||||
};
|
||||
|
||||
// Determine if we should show icons only (both URLs configured)
|
||||
const showIconsOnly = Boolean(calibreWebUrl && audiobookLibraryUrl);
|
||||
|
||||
// Icon buttons component - reused for both states
|
||||
const IconButtons = () => (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Calibre-Web Button */}
|
||||
{/* Book Library Button */}
|
||||
{calibreWebUrl && (
|
||||
<a
|
||||
href={calibreWebUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-full hover-action transition-all duration-200 text-gray-900 dark:text-gray-100"
|
||||
aria-label="Open Calibre-Web"
|
||||
title="Go To Library"
|
||||
aria-label="Open book library"
|
||||
title={showIconsOnly ? "Book Library" : "Go To Library"}
|
||||
>
|
||||
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">Go To Library</span>
|
||||
{!showIconsOnly && <span className="text-sm font-medium">Go To Library</span>}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{/* Audiobook Library Button */}
|
||||
{audiobookLibraryUrl && (
|
||||
<a
|
||||
href={audiobookLibraryUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-full hover-action transition-all duration-200 text-gray-900 dark:text-gray-100"
|
||||
aria-label="Open audiobook library"
|
||||
title={showIconsOnly ? "Audiobook Library" : "Go To Library"}
|
||||
>
|
||||
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
|
||||
</svg>
|
||||
{!showIconsOnly && <span className="text-sm font-medium">Go To Library</span>}
|
||||
</a>
|
||||
)}
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ export type ContentType = 'ebook' | 'audiobook';
|
||||
|
||||
export interface AppConfig {
|
||||
calibre_web_url: string;
|
||||
audiobook_library_url: string;
|
||||
debug: boolean;
|
||||
build_version: string;
|
||||
release_version: string;
|
||||
|
||||
Reference in New Issue
Block a user