mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-06 09:24:40 +01:00
fix(packs): don't disrupt normal downloads when inspecting for packs (#1274)
Follow-ups to the multi-book pack feature (#1270), which inspects every release before download. Two behaviours leaked into the ordinary single-book flow and are corrected here: - A flat folder of chaptered audio (`01 - Chapter.mp3`, `02 - ...`) was detected as a pack, because each track name parses to a series position, so clicking download popped the review panel for one normal audiobook. Flat folders are now split one-book-per-file only with real evidence of distinct books: two or more series positions, more than one title, and no chaptered audio (only the single-file m4b/m4a containers and ebook formats qualify). Subfolder packs and flat m4b/m4a packs are unchanged. - Every release that couldn't be inspected (usenet, magnet-only, sources without a list_files hook, ABB single-file) showed an info toast on download. That is now a console.warn, so a normal download is silent again. Adds regression tests for the chaptered-mp3 cases.
This commit is contained in:
@@ -14,6 +14,16 @@ import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
from shelfmark.core.utils import AUDIOBOOK_FORMATS
|
||||
|
||||
# m4b/m4a hold a whole audiobook in one file; every other audio format (mp3, flac, ...) is
|
||||
# chaptered - many files make up one book. Ebook formats are always one file per book, so
|
||||
# only chaptered *audio* matters here. A flat folder is split one-book-per-file only when
|
||||
# none of its files are chaptered audio: a bare list of `01 - Chapter.mp3` tracks is a
|
||||
# single chaptered audiobook, not a pack of books.
|
||||
_SINGLE_FILE_AUDIO_CONTAINERS = frozenset({"m4b", "m4a"})
|
||||
_CHAPTERED_AUDIO_EXTENSIONS = frozenset(AUDIOBOOK_FORMATS) - _SINGLE_FILE_AUDIO_CONTAINERS
|
||||
|
||||
_YEAR_SUFFIX_RE = re.compile(r"\s*\(\s*(?P<year>\d{4})\s*\)\s*$")
|
||||
_SERIES_MARKER_RE = re.compile(
|
||||
r"""
|
||||
@@ -276,7 +286,14 @@ def plan_pack(
|
||||
for f in root_files
|
||||
]
|
||||
positions = {p[1] for p in parsed if p[1] is not None}
|
||||
if len(positions) >= 2:
|
||||
titles = {p[0].strip().lower() for p in parsed if p[0]}
|
||||
one_book_per_file = all(
|
||||
rel.suffix.lower().lstrip(".") not in _CHAPTERED_AUDIO_EXTENSIONS for rel in root_files
|
||||
)
|
||||
# Split a flat folder into a book per file only with real evidence of distinct
|
||||
# books: two or more series positions, more than one title, and no chaptered audio
|
||||
# (a bare list of `01 - Chapter.mp3` tracks is one book, not a pack).
|
||||
if len(positions) >= 2 and len(titles) >= 2 and one_book_per_file:
|
||||
books.extend(
|
||||
PackBook(title=title, series_position=position, year=year, files=[str(f)])
|
||||
for f, (title, position, year) in zip(root_files, parsed, strict=True)
|
||||
|
||||
@@ -1220,12 +1220,14 @@ const ReleaseModalSession = ({
|
||||
// reviewed and filed as separate books instead of one mangled item.
|
||||
let inspected = false;
|
||||
let plan: PackPlan | null = null;
|
||||
let reason: string | null = null;
|
||||
try {
|
||||
const inspection = await inspectRelease(
|
||||
buildReleaseDownloadPayload(book, release, contentType),
|
||||
);
|
||||
inspected = inspection.inspected;
|
||||
plan = inspection.plan;
|
||||
reason = inspection.reason;
|
||||
} catch (error) {
|
||||
console.error('Release inspection failed:', error);
|
||||
}
|
||||
@@ -1233,10 +1235,14 @@ const ReleaseModalSession = ({
|
||||
setPackReview({ release, plan, books: plan.books });
|
||||
return;
|
||||
}
|
||||
// Not a pack (or couldn't be inspected): queue exactly as before. A release we
|
||||
// couldn't inspect might still be an unnoticed pack, so leave a console breadcrumb
|
||||
// rather than interrupting the user; the multi-book toggle forces the split.
|
||||
if (!inspected && !multiBook) {
|
||||
onShowToast?.(
|
||||
"Couldn't check this release's files before download. If it contains several books, turn on the multi-book pack toggle first.",
|
||||
'info',
|
||||
console.warn(
|
||||
`Could not inspect release "${release.title}" before download${
|
||||
reason ? `: ${reason}` : ''
|
||||
}. If it contains several books, enable the multi-book pack toggle.`,
|
||||
);
|
||||
}
|
||||
await onDownload(book, release, contentType, multiBook ? { multiBook: true } : {});
|
||||
@@ -1259,7 +1265,6 @@ const ReleaseModalSession = ({
|
||||
contentType,
|
||||
handleClose,
|
||||
multiBook,
|
||||
onShowToast,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -150,6 +150,33 @@ class TestPlanPack:
|
||||
]
|
||||
assert plan.ignored == ["The Expanse 1.0 - Leviathan Wakes (2011).txt"]
|
||||
|
||||
def test_flat_chaptered_mp3_tracks_are_one_book_not_a_pack(self):
|
||||
# A single audiobook whose chapters are named "01 - <chapter>.mp3" must not be
|
||||
# split into one "book" per track just because each name carries a number.
|
||||
files = [
|
||||
PackFile("The Hobbit/01 - An Unexpected Party.mp3", 10),
|
||||
PackFile("The Hobbit/02 - Roast Mutton.mp3", 10),
|
||||
PackFile("The Hobbit/03 - A Short Rest.mp3", 10),
|
||||
]
|
||||
plan = plan_pack(files, supported_extensions=AUDIO, series_name=None)
|
||||
assert not plan.is_pack
|
||||
assert len(plan.books) == 1
|
||||
assert plan.books[0].files == [
|
||||
"The Hobbit/01 - An Unexpected Party.mp3",
|
||||
"The Hobbit/02 - Roast Mutton.mp3",
|
||||
"The Hobbit/03 - A Short Rest.mp3",
|
||||
]
|
||||
|
||||
def test_flat_repeated_title_mp3_tracks_are_one_book(self):
|
||||
# Same title on every track ("01 - The Hobbit.mp3") is a chaptered book too.
|
||||
files = [
|
||||
PackFile("01 - The Hobbit.mp3", 10),
|
||||
PackFile("02 - The Hobbit.mp3", 10),
|
||||
]
|
||||
plan = plan_pack(files, supported_extensions=AUDIO, series_name=None)
|
||||
assert not plan.is_pack
|
||||
assert len(plan.books) == 1
|
||||
|
||||
def test_audiobookbay_flat_list_with_trailing_markers_is_a_pack(self):
|
||||
# ABB's file table has no folder separators: "<folder> <file> <size>".
|
||||
files = [
|
||||
|
||||
Reference in New Issue
Block a user