mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Implement existing item lookup and overwrite handling for Audiobookshelf uploads
This commit is contained in:
@@ -8,6 +8,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from contextlib import ExitStack
|
||||
from dataclasses import dataclass
|
||||
@@ -107,6 +108,7 @@ _ACRONYM_ALLOWLIST = {
|
||||
}
|
||||
_ROMAN_NUMERAL_CHARS = frozenset("IVXLCDM")
|
||||
_CAPS_WORD_RE = re.compile(r"[A-Z][A-Z0-9'\u2019-]*")
|
||||
_OUTPUT_SANITIZE_RE = re.compile(r"[^\w\-_.]+")
|
||||
|
||||
|
||||
def _simplify_heading_text(text: str) -> str:
|
||||
@@ -1844,7 +1846,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
|
||||
if audio_asset:
|
||||
try:
|
||||
epub_root = project_root if job.save_as_project else base_output_dir
|
||||
epub_root = project_root
|
||||
epub_output_path = _build_output_path(epub_root, job.original_filename, "epub")
|
||||
job.add_log("Generating EPUB 3 package with synchronized narration…")
|
||||
epub_path = build_epub3_package(
|
||||
@@ -1989,21 +1991,19 @@ def _prepare_output_dir(job: Job) -> Path:
|
||||
|
||||
|
||||
def _build_output_path(directory: Path, original_name: str, extension: str) -> Path:
|
||||
base_name = Path(original_name).stem
|
||||
sanitized = re.sub(r"[^\w\-_.]+", "_", base_name).strip("_") or "output"
|
||||
candidate = directory / f"{sanitized}.{extension}"
|
||||
counter = 1
|
||||
while candidate.exists():
|
||||
candidate = directory / f"{sanitized}_{counter}.{extension}"
|
||||
counter += 1
|
||||
return candidate
|
||||
sanitized = _sanitize_output_stem(original_name)
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
return directory / f"{sanitized}.{extension}"
|
||||
|
||||
|
||||
def _prepare_project_layout(job: Job, base_dir: Path) -> tuple[Path, Path, Path, Optional[Path]]:
|
||||
base_dir.mkdir(parents=True, exist_ok=True)
|
||||
stem = Path(job.original_filename).stem
|
||||
sanitized = _sanitize_output_stem(job.original_filename)
|
||||
folder_name = f"{_output_timestamp_token()}_{sanitized}"
|
||||
project_root = base_dir / folder_name
|
||||
project_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if job.save_as_project:
|
||||
project_root = _ensure_unique_directory(base_dir, f"{stem}_project")
|
||||
audio_dir = project_root / "audio"
|
||||
subtitle_dir = project_root / "subtitles"
|
||||
metadata_dir = project_root / "metadata"
|
||||
@@ -2011,17 +2011,7 @@ def _prepare_project_layout(job: Job, base_dir: Path) -> tuple[Path, Path, Path,
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
return project_root, audio_dir, subtitle_dir, metadata_dir
|
||||
|
||||
return base_dir, base_dir, base_dir, None
|
||||
|
||||
|
||||
def _ensure_unique_directory(parent: Path, name: str) -> Path:
|
||||
candidate = parent / name
|
||||
counter = 1
|
||||
while candidate.exists():
|
||||
candidate = parent / f"{name}_{counter}"
|
||||
counter += 1
|
||||
candidate.mkdir(parents=True, exist_ok=True)
|
||||
return candidate
|
||||
return project_root, project_root, project_root, None
|
||||
|
||||
|
||||
def _apply_newline_policy(chapters: List[ExtractedChapter], replace_single_newlines: bool) -> None:
|
||||
@@ -2039,6 +2029,16 @@ def _slugify(title: str, index: int) -> str:
|
||||
return sanitized[:80]
|
||||
|
||||
|
||||
def _sanitize_output_stem(name: str) -> str:
|
||||
base = Path(name or "").stem
|
||||
sanitized = _OUTPUT_SANITIZE_RE.sub("_", base).strip("_")
|
||||
return sanitized or "output"
|
||||
|
||||
|
||||
def _output_timestamp_token() -> str:
|
||||
return datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
|
||||
def _open_audio_sink(
|
||||
path: Path,
|
||||
job: Job,
|
||||
|
||||
+47
-1
@@ -5064,10 +5064,56 @@ def send_job_to_audiobookshelf(job_id: str) -> ResponseReturnValue:
|
||||
subtitles = _existing_paths(job.result.subtitle_paths) if config.send_subtitles else None
|
||||
chapters = _load_audiobookshelf_chapters(job) if config.send_chapters else None
|
||||
metadata = _build_audiobookshelf_metadata(job)
|
||||
display_title = metadata.get("title") or audio_path.stem
|
||||
overwrite_requested = request.form.get("overwrite") == "true" or request.args.get("overwrite") == "true"
|
||||
|
||||
try:
|
||||
client = AudiobookshelfClient(config)
|
||||
except ValueError as exc: # pragma: no cover - defensive guard
|
||||
job.add_log(f"Audiobookshelf configuration error: {exc}", level="error")
|
||||
service._persist_state()
|
||||
return _panel_response()
|
||||
|
||||
try:
|
||||
existing_items = client.find_existing_items(display_title, folder_id=config.folder_id)
|
||||
except AudiobookshelfUploadError as exc:
|
||||
job.add_log(f"Audiobookshelf lookup failed: {exc}", level="error")
|
||||
service._persist_state()
|
||||
return _panel_response()
|
||||
|
||||
if existing_items and not overwrite_requested:
|
||||
job.add_log(
|
||||
f"Audiobookshelf already contains '{display_title}'. Awaiting overwrite confirmation.",
|
||||
level="warning",
|
||||
)
|
||||
service._persist_state()
|
||||
if request.headers.get("HX-Request"):
|
||||
detail = {
|
||||
"jobId": job.id,
|
||||
"title": display_title,
|
||||
"url": url_for("web.send_job_to_audiobookshelf", job_id=job.id),
|
||||
"target": request.headers.get("HX-Target") or "#jobs-panel",
|
||||
"message": f'Audiobookshelf already contains "{display_title}". Overwrite?',
|
||||
}
|
||||
headers = {"HX-Trigger": json.dumps({"audiobookshelf-overwrite-prompt": detail})}
|
||||
return Response("", status=204, headers=headers)
|
||||
return _panel_response()
|
||||
|
||||
if existing_items and overwrite_requested:
|
||||
try:
|
||||
client.delete_items(existing_items)
|
||||
except AudiobookshelfUploadError as exc:
|
||||
job.add_log(f"Audiobookshelf overwrite aborted: {exc}", level="error")
|
||||
service._persist_state()
|
||||
return _panel_response()
|
||||
else:
|
||||
job.add_log(
|
||||
f"Removed {len(existing_items)} existing Audiobookshelf item(s) prior to overwrite.",
|
||||
level="info",
|
||||
)
|
||||
|
||||
job.add_log("Audiobookshelf upload triggered manually.", level="info")
|
||||
try:
|
||||
client = AudiobookshelfClient(config)
|
||||
client.upload_audiobook(
|
||||
audio_path,
|
||||
metadata=metadata,
|
||||
|
||||
@@ -999,6 +999,21 @@ class ConversionService:
|
||||
metadata = _build_audiobookshelf_metadata(job)
|
||||
|
||||
client = AudiobookshelfClient(config)
|
||||
|
||||
display_title = metadata.get("title") or audio_path.stem
|
||||
try:
|
||||
existing_items = client.find_existing_items(display_title, folder_id=config.folder_id)
|
||||
except AudiobookshelfUploadError as exc:
|
||||
job.add_log(f"Audiobookshelf lookup failed: {exc}", level="error")
|
||||
return
|
||||
|
||||
if existing_items:
|
||||
job.add_log(
|
||||
f"Audiobookshelf upload skipped: '{display_title}' already exists in the configured folder.",
|
||||
level="warning",
|
||||
)
|
||||
return
|
||||
|
||||
client.upload_audiobook(
|
||||
audio_path,
|
||||
metadata=metadata,
|
||||
|
||||
@@ -1,7 +1,41 @@
|
||||
import { initReaderUI } from "./reader.js";
|
||||
|
||||
const queueState = (window.AbogenQueueState = window.AbogenQueueState || {
|
||||
boundOverwritePrompt: false,
|
||||
});
|
||||
|
||||
const handleOverwritePrompt = (event) => {
|
||||
const detail = event?.detail || {};
|
||||
const title = detail.title || "this item";
|
||||
const message = detail.message || `Audiobookshelf already has "${title}". Overwrite?`;
|
||||
if (!window.confirm(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = detail.url;
|
||||
if (!url || typeof htmx === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = detail.target || "#jobs-panel";
|
||||
const values = { overwrite: "true" };
|
||||
if (detail.values && typeof detail.values === "object") {
|
||||
Object.assign(values, detail.values);
|
||||
}
|
||||
|
||||
htmx.ajax("POST", url, {
|
||||
target,
|
||||
swap: "innerHTML",
|
||||
values,
|
||||
});
|
||||
};
|
||||
|
||||
const initQueuePage = () => {
|
||||
initReaderUI();
|
||||
if (!queueState.boundOverwritePrompt) {
|
||||
queueState.boundOverwritePrompt = true;
|
||||
document.addEventListener("audiobookshelf-overwrite-prompt", handleOverwritePrompt);
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
|
||||
Reference in New Issue
Block a user