mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-24 20:50:28 +01:00
- Added the manual retry option for failed downloads
- Added the ability to retry failed post-processing using existing
downloaded file
- Added admin-visible "Download as" selector, admin chooses a user to
download on-behalf of - inherits their output preferences.
- Added search mode and default metadata provider / release source
options to User Preferences and My Account settings.
- Added sort by format option in release results
- Added {OriginalName} renaming field option, to retain the exact
downloaded filename
- Frontend dependency updates - fixes rollup vulnerability from this
week
Closes #662 #656 #649 #562
186 lines
6.2 KiB
Python
186 lines
6.2 KiB
Python
"""Data structures and models used across the application."""
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
from enum import Enum
|
|
import re
|
|
import time
|
|
|
|
|
|
def build_filename(
|
|
title: str,
|
|
author: Optional[str] = None,
|
|
year: Optional[str] = None,
|
|
fmt: Optional[str] = None,
|
|
) -> str:
|
|
parts = []
|
|
if author:
|
|
parts.append(author)
|
|
parts.append(" - ")
|
|
parts.append(title)
|
|
if year:
|
|
parts.append(f" ({year})")
|
|
|
|
filename = "".join(parts)
|
|
filename = re.sub(r'[\\/:*?"<>|]', '_', filename.strip())[:245]
|
|
|
|
if fmt:
|
|
filename = f"{filename}.{fmt}"
|
|
|
|
return filename
|
|
|
|
|
|
class QueueStatus(str, Enum):
|
|
"""Enum for possible book queue statuses."""
|
|
QUEUED = "queued"
|
|
RESOLVING = "resolving"
|
|
LOCATING = "locating"
|
|
DOWNLOADING = "downloading"
|
|
COMPLETE = "complete"
|
|
AVAILABLE = "available"
|
|
ERROR = "error"
|
|
DONE = "done"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
class SearchMode(str, Enum):
|
|
DIRECT = "direct"
|
|
UNIVERSAL = "universal"
|
|
|
|
|
|
@dataclass
|
|
class QueueItem:
|
|
"""Queue item with priority and metadata."""
|
|
book_id: str
|
|
priority: int
|
|
added_time: float
|
|
|
|
def __lt__(self, other):
|
|
"""Compare items for priority queue (lower priority number = higher precedence)."""
|
|
if self.priority != other.priority:
|
|
return self.priority < other.priority
|
|
return self.added_time < other.added_time
|
|
|
|
|
|
@dataclass
|
|
class DownloadTask:
|
|
task_id: str # Unique ID (e.g., AA MD5 hash, Prowlarr GUID)
|
|
source: str # Handler name ("direct_download", "prowlarr")
|
|
title: str # Display title for queue sidebar
|
|
|
|
# Display info for queue sidebar
|
|
author: Optional[str] = None
|
|
year: Optional[str] = None
|
|
format: Optional[str] = None
|
|
size: Optional[str] = None
|
|
preview: Optional[str] = None
|
|
content_type: Optional[str] = None # "book (fiction)", "audiobook", "magazine", etc.
|
|
source_url: Optional[str] = None # Original release URL used by source-specific handlers
|
|
|
|
# Series info (for library naming templates)
|
|
series_name: Optional[str] = None
|
|
series_position: Optional[float] = None # Float for novellas (e.g., 1.5)
|
|
subtitle: Optional[str] = None # Book subtitle for naming templates
|
|
|
|
# Hardlinking support
|
|
original_download_path: Optional[str] = None # Path in download client (for hardlinking)
|
|
|
|
# Search mode - determines post-download processing behavior
|
|
# See SearchMode enum for behavioral differences
|
|
search_mode: Optional[SearchMode] = None
|
|
|
|
# Output selection for post-processing.
|
|
# This is captured at queue time so in-flight tasks are not affected if the user changes settings later.
|
|
output_mode: Optional[str] = None # e.g. "folder", "booklore", "email"
|
|
output_args: Dict[str, Any] = field(default_factory=dict) # Per-output parameters (e.g. email recipient)
|
|
|
|
# User association (multi-user support)
|
|
user_id: Optional[int] = None # DB user ID who queued this download
|
|
username: Optional[str] = None # Username for {User} template variable
|
|
request_id: Optional[int] = None # Origin request ID when queued from request fulfilment
|
|
|
|
# Runtime state
|
|
priority: int = 0
|
|
added_time: float = field(default_factory=time.time)
|
|
progress: float = 0.0
|
|
status: QueueStatus = QueueStatus.QUEUED
|
|
status_message: Optional[str] = None
|
|
download_path: Optional[str] = None
|
|
last_error_message: Optional[str] = None
|
|
last_error_type: Optional[str] = None
|
|
staged_path: Optional[str] = None
|
|
|
|
def __lt__(self, other):
|
|
"""Compare tasks for priority queue (lower priority number = higher precedence)."""
|
|
if self.priority != other.priority:
|
|
return self.priority < other.priority
|
|
return self.added_time < other.added_time
|
|
|
|
def get_filename(self) -> str:
|
|
"""Build sanitized filename from task metadata."""
|
|
if self.download_path:
|
|
return Path(self.download_path).name
|
|
return build_filename(self.title, self.author, self.year, self.format)
|
|
|
|
|
|
@dataclass
|
|
class BookInfo:
|
|
"""Data class representing book information."""
|
|
id: str
|
|
title: str
|
|
preview: Optional[str] = None
|
|
author: Optional[str] = None
|
|
publisher: Optional[str] = None
|
|
year: Optional[str] = None
|
|
language: Optional[str] = None
|
|
content: Optional[str] = None
|
|
format: Optional[str] = None
|
|
size: Optional[str] = None
|
|
info: Optional[Dict[str, List[str]]] = None
|
|
description: Optional[str] = None
|
|
download_urls: List[str] = field(default_factory=list)
|
|
download_path: Optional[str] = None
|
|
priority: int = 0
|
|
progress: Optional[float] = None
|
|
status_message: Optional[str] = None # Detailed status message for UI display
|
|
added_time: Optional[float] = None # Timestamp when added to queue
|
|
source: str = "direct_download" # Release source handler to use for downloads
|
|
source_url: Optional[str] = None # Link to source page (e.g., Anna's Archive)
|
|
|
|
def get_filename(self, fallback_url: Optional[str] = None) -> str:
|
|
"""Build sanitized filename: 'Author - Title (Year).format'
|
|
|
|
Resolves format from self.format, download_urls, or fallback_url.
|
|
|
|
Args:
|
|
fallback_url: URL to extract format from if not already known
|
|
|
|
Returns:
|
|
Sanitized filename safe for filesystem use
|
|
"""
|
|
# Resolve format if needed
|
|
if not self.format:
|
|
urls = [self.download_urls[0]] if self.download_urls else []
|
|
if fallback_url:
|
|
urls.append(fallback_url)
|
|
for url in urls:
|
|
ext = url.split(".")[-1].lower()
|
|
if ext and len(ext) <= 5 and ext.isalnum():
|
|
self.format = ext
|
|
break
|
|
|
|
return build_filename(self.title, self.author, self.year, self.format)
|
|
|
|
|
|
@dataclass
|
|
class SearchFilters:
|
|
"""Filters for book search queries."""
|
|
isbn: Optional[List[str]] = None
|
|
author: Optional[List[str]] = None
|
|
title: Optional[List[str]] = None
|
|
lang: Optional[List[str]] = None
|
|
sort: Optional[str] = None
|
|
content: Optional[List[str]] = None
|
|
format: Optional[List[str]] = None
|