diff --git a/CHANGELOG.md b/CHANGELOG.md index b49e749..55871e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ point is tag `upstream-baseline`. ## Unreleased ### Added +- `Config/target_path/in_series_no_part` (default `{author}/{series}/{series} - {title}`): the template for a + book in a series whose part is unknown. `in_series` rendered `{part}` empty and produced folders such as + `Jack Reacher # - Three More Jack Reacher Novellas` (upstream #27); two unnumbered entries of one series could + collide. Set it to your `in_series` value to keep the old names. Books already filed are not moved. - `--pin RELEASE=ASIN` (repeatable; `Config/pins`): use that Audible product for the release and re-process it now, ignoring its cached answers and processed marker. Shorthand for a hint `{"asin": ..., "refresh": true}`; a malformed pin exits 2, an unused one is reported at the end of the run. diff --git a/CONFIG.md b/CONFIG.md index c28135c..f48eed9 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -32,6 +32,7 @@ A copy of default_config.cfg can be found under the /templates folder. It is re "target_path": { "multi_author": "{first_author}", "in_series": "{author}/{series}/{series} #{part} - {title}", + "in_series_no_part": "{author}/{series}/{series} - {title}", "no_series": "{author}/{title}", "disc_folder": "{title} {disc}" }, @@ -92,6 +93,7 @@ A copy of default_config.cfg can be found under the /templates folder. It is re | target_path | | | | | | multi_author | How to handle the Author folder for multi-author books: first_author, authors, "", "Static folder name" | first_author | | | in_series | Format of the generated tree for books in a series | {author}/{series}/{series} #{part} - {title} | +| | in_series_no_part | Format for books in a series whose part number is unknown (a novella collection, a companion book): `in_series` would render `{part}` empty and leave `Series # - Title`. Set it to your `in_series` value to keep that | {author}/{series}/{series} - {title} | | | no_series | Format of the generated tree for books that are NOT in a series | {author}/{title} | | | disc_folder | Format of the folder name for multi-disc books (files whose parent folder is `cd N`, `disc N`, `disk N` or `part N`) | {title} {disc} | | tokens | | | | diff --git a/myx_classes.py b/myx_classes.py index a7d79b5..08e4726 100644 --- a/myx_classes.py +++ b/myx_classes.py @@ -27,6 +27,10 @@ class Contributor: name:str #books:list[int]= field(default_factory=list) +#target_path template for a book in a series whose part is unknown (Config/target_path/in_series_no_part) +IN_SERIES_NO_PART = "{author}/{series}/{series} - {title}" + + #Series Class @dataclass class Series: @@ -307,6 +311,7 @@ def getConfigTargetPath(self, cfg, book): multi_author = cfg.get("Config/target_path/multi_author") in_series = cfg.get("Config/target_path/in_series") no_series = cfg.get("Config/target_path/no_series") + in_series_no_part = cfg.get("Config/target_path/in_series_no_part") or IN_SERIES_NO_PART disc_folder = cfg.get("Config/target_path/disc_folder") if (book is not None): @@ -367,7 +372,10 @@ def getConfigTargetPath(self, cfg, book): sPath = "" if len(book.series): - x = in_series.format (**tokens) + #a series entry without a part (a novella collection, a companion) would render "{part}" empty and + #leave "Series # - Title" behind (upstream #27): such books use in_series_no_part instead + template = in_series if len(tokens["part"]) else in_series_no_part + x = template.format (**tokens) #use in_series format for p in x.split ("/"): sPath=os.path.join (sPath, p.strip()) diff --git a/templates/default_config.cfg b/templates/default_config.cfg index a200d49..c6f3cfa 100644 --- a/templates/default_config.cfg +++ b/templates/default_config.cfg @@ -40,6 +40,7 @@ "target_path": { "multi_author": "{first_author}", "in_series": "{author}/{series}/{series} #{part} - {title}", + "in_series_no_part": "{author}/{series}/{series} - {title}", "no_series": "{author}/{title}", "disc_folder": "{title} {disc}", "calibre_ingest_path": "/path/to/calibre/upload" diff --git a/tests/test_names.py b/tests/test_names.py index e7d7b9b..f8a51dc 100644 --- a/tests/test_names.py +++ b/tests/test_names.py @@ -315,3 +315,30 @@ def test_grouped_discs_get_distinct_target_folders(self): self.assertEqual(len(targets), 4) self.assertIn("/lib/Author/Title/Title Disk 1", targets) + + +class InSeriesNoPartTest(unittest.TestCase): + def target(self, part, **over): + import myx_classes + book = myx_classes.Book(asin="B000000001", title="Three More Novellas") + book.authors = [myx_classes.Contributor("Lee Child")] + book.series = [myx_classes.Series("Jack Reacher", part)] + cfg = FakeConfig("/tmp", **{"Config/target_path/no_series": "{author}/{title}", **over}) + bf = myx_classes.BookFile("x.m4b", "/dl/x.m4b", "/dl", "/lib") + return bf.getConfigTargetPath(cfg, book) + + def test_series_without_a_part_uses_the_no_part_template(self): + # upstream #27: "Jack Reacher # - Three More Novellas" + self.assertEqual(self.target(""), "/lib/Lee Child/Jack Reacher/Jack Reacher - Three More Novellas") + self.assertEqual(self.target(" "), "/lib/Lee Child/Jack Reacher/Jack Reacher - Three More Novellas") + self.assertEqual(self.target("23.5"), "/lib/Lee Child/Jack Reacher/Jack Reacher #23.5 - Three More Novellas") + self.assertEqual(self.target("3"), "/lib/Lee Child/Jack Reacher/Jack Reacher #3 - Three More Novellas") + + def test_template_is_configurable_and_the_old_layout_can_be_kept(self): + self.assertEqual(self.target("", **{"Config/target_path/in_series_no_part": "{author}/{series}/{title}"}), + "/lib/Lee Child/Jack Reacher/Three More Novellas") + old = "{author}/{series}/{series} #{part} - {title}" + self.assertEqual(self.target("", **{"Config/target_path/in_series_no_part": old}), + "/lib/Lee Child/Jack Reacher/Jack Reacher # - Three More Novellas") + self.assertEqual(self.target("", **{"Config/target_path/in_series_no_part": ""}), # blank = default + "/lib/Lee Child/Jack Reacher/Jack Reacher - Three More Novellas")