Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ point is tag `upstream-baseline`.
still appends a `run` record carrying `exit_code` and `error`.

### Fixed
- Series parts survive a trip through the run log. The log writes `seriesparts` as `Name part` while the
log-mode reader (`fix.csv`) split on `#`, so the whole string became the series name and the part was lost
(upstream #27, half of it). The reader now pairs `seriesparts` with the `series` column; a decimal part is
logged as `17.5` instead of `17 5` (the only visible change in the corpus replay: the fuzzy-match string of four
novellas, same matches).
- `Config/dedupe_roots`: a multi-file release whose discs are already hardlinked under *different* library
folders (a prior `multibook` run, or a file moved in the library) is no longer reported as already filed.
The first folder used to win, `hardlinkUnlessFiled` skipped, and the processed marker then hid the rest of
Expand Down
2 changes: 1 addition & 1 deletion booktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def buildTreeFromLog(files, logfile, cfg):
bf.isMatched = (str(row["isMatched"]).lower() == "true")
bf.ffprobeBook.setAuthors(row["id3-authors"])
bf.ffprobeBook.setNarrators(row["id3-narrators"])
bf.ffprobeBook.setSeries(row["id3-seriesparts"])
bf.ffprobeBook.setSeriesFromLog(row.get("id3-series", ""), row["id3-seriesparts"])

#does this book exist?
hashKey=myx_utilities.getHash(f"{i}-{row['book']}")
Expand Down
2 changes: 1 addition & 1 deletion docs/FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Scripts built on upstream booktree keep working:
| 3 | Missing id3 → the whole filename is the search title (`Author - Title.m4b` scores 41 against its own MAM entry). Fixed: release-name parsing (`flags/parse_names`, `--legacy-names` for the old behaviour), see CONFIG.md | #26 |
| 4 | Empty Audible / MAM results and skeleton per-ASIN responses (`{asin, asset_details, is_vvab}`) are cached forever. Fixed: TTLs by kind and emptiness, errors never cached, `--refresh`, `--json-log`; MAM requests spaced ≥ 6 s and capped per run (see CONFIG.md) | #25 |
| 5 | MAM session duplicated in every config file and kept in a pickle; `title_patterns` contain `"\b"` JSON escapes that become backspaces; exit code is 0 on failure. Fixed: `MAM_SESSION` / `MAM_SESSION_FILE`, JSON cookie store, backspace patterns repaired, exit codes 0/1/2 (see CONFIG.md) | — |
| 6 | `Series # - Title` folders: the log writes `seriesparts` as `Name part`, the log reader splits on `#` | #27 |
| 6 | `Series # - Title` folders: the log writes `seriesparts` as `Name part`, the log reader splits on `#`. Fixed: the reader pairs `seriesparts` with `series`, decimal parts are logged as `17.5`, and a series entry without a part is filed with `target_path/in_series_no_part` (default `Series - Title`) | #27 |
| 6b | Multi-disc releases (`cd1/`, `Disc 01/`) are grouped per disc folder, so each disc is matched on its own, the runtime evidence is one disc long (the wrong edition can win), and `book` logs as `cd1..cdN`. Two releases that both use `cd1/` were also merged into one book. Fixed: grouping walks past disc parents to the release folder | #26 |
| 8 | Everything around a run lived in per-host wrapper scripts (ntfy summary, Audiobookshelf scan, inode de-dupe, an ASIN fixer that rewrote `fix.csv`). Added as config, off by default: `notify`, `abs`, `dedupe_roots`, `--pin RELEASE=ASIN` with `--remember` to keep the correction in the hints file (see "Correcting a match" in CONFIG.md) | — |
| 7 | mousehole cookie integration. Fixed: `mousehole_state_file` / `MOUSEHOLE_STATE_FILE`, reads mousehole's v2 (`cookie`) and legacy (`currentCookie`) state files (PR #24 read only the legacy key) | #24 |
Expand Down
33 changes: 30 additions & 3 deletions myx_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,14 @@ def getNarrators(self, delimiter=",", encloser="", stripaccents=True):
return ""

def getSeriesParts(self, delimiter=",", encloser="", stripaccents=True):
#the name is cleansed like a contributor (as upstream did); the part is kept as is, so that a decimal
#part such as 17.5 is logged as 17.5 and not as "17 5"
seriesparts = []
for s in self.series:
if len(s.name.strip()):
seriesparts.append(Contributor(f"{s.name} {s.separator}{s.part}"))

return myx_utilities.getList(seriesparts, delimiter, encloser, stripaccents=True)
text = " ".join(f"{myx_utilities.cleanseAuthor(s.name)} {s.separator}{s.part}".split())
seriesparts.append(f"{encloser}{text}{encloser}")
return delimiter.join(seriesparts)

def setAuthors(self, authors):
#Given a csv of authors, convert it to a list
Expand All @@ -145,6 +147,31 @@ def setSeries(self, series):
else:
self.series.append(Series(str(p[0]).strip(), ""))

def setSeriesFromLog(self, series, seriesparts):
"""Rebuild the series from a run-log row. The log writes the names in `series` and "Name part" in
`seriesparts` without a `#`, so the part can only be told from a name that itself ends in a number
("Area 51") with the names at hand. Without names (a hand-written row) fall back to `Name #part`."""
names = [n.strip() for n in str(series or "").split(",") if n.strip()]
parts = [p.strip() for p in str(seriesparts or "").split(",") if p.strip()]
if not names:
return self.setSeries(str(seriesparts or ""))
#`series` went through cleanseSeries, `seriesparts` through cleanseAuthor: compare both the same way
norm = lambda text: myx_utilities.cleanseAuthor(myx_utilities.cleanseSeries(text)) # noqa: E731
for name in names:
key = norm(name)
part = ""
for p in parts:
q = norm(p)
if q == key:
break
if q.startswith(key + " ") or q.startswith(key + "#"):
part = q[len(key):].strip().lstrip("#").strip()
m = re.fullmatch(r"(\d+) (\d+)", part) # "17 5": a decimal part logged before the fix
if m:
part = f"{m.group(1)}.{m.group(2)}"
break
self.series.append(Series(name, part))

def getDictionary(self, book, ns=""):
book[f"{ns}matchRate"]=self.matchRate
book[f"{ns}asin"]=self.asin
Expand Down
40 changes: 40 additions & 0 deletions tests/test_logmode.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,43 @@ def run_once(**over):
self.assertIn("Applying hint for junk: {'refresh': True}", third)
self.assertIn("Pinned ASIN B0CC3NZ34S accepted", third)
self.assertEqual(len(booktree.httpx.calls) if hasattr(booktree.httpx, "calls") else 2, 2)


class SeriesRoundTripTest(unittest.TestCase):
"""upstream #27: the log writes seriesparts as "Name part" (no '#'), the reader split on '#' and lost the part."""

def test_series_part_survives_a_trip_through_the_log(self):
import myx_classes
src = myx_classes.Book(title="The Honeymoon Heist")
src.series = [myx_classes.Series("Pike Logan", "17.5"), myx_classes.Series("Area 51", ""), myx_classes.Series("Dune", "1")]
row = {}
src.getDictionary(row, "id3-")
self.assertEqual(row["id3-series"], "Pike Logan,Area 51,Dune")
self.assertEqual(row["id3-seriesparts"], "Pike Logan 17.5,Area 51,Dune 1") # CSV value unchanged
back = myx_classes.Book()
back.setSeriesFromLog(row["id3-series"], row["id3-seriesparts"])
self.assertEqual([(s.name, s.part) for s in back.series], [("Pike Logan", "17.5"), ("Area 51", ""), ("Dune", "1")])
self.assertEqual(back.getSeriesParts(), src.getSeriesParts())
# a row written before the fix ("17 5"), and names the two columns cleanse differently
back = myx_classes.Book()
back.setSeriesFromLog("Pike Logan,Hitchhiker's Guide: Trilogy", "Pike Logan 17 5,Hitchhikers Guide: Trilogy 2")
self.assertEqual([(s.name, s.part) for s in back.series], [("Pike Logan", "17.5"), ("Hitchhiker's Guide: Trilogy", "2")])
# the old reader turned the whole string into a series named "Pike Logan 17.5"
legacy = myx_classes.Book()
legacy.setSeries("Pike Logan 17.5")
self.assertEqual([(s.name, s.part) for s in legacy.series], [("Pike Logan 17.5", "")])

def test_hand_written_rows_still_accept_name_hash_part(self):
import myx_classes
b = myx_classes.Book()
b.setSeriesFromLog("", "Jack Reacher #3") # no series column: legacy '#' form
self.assertEqual([(s.name, s.part) for s in b.series], [("Jack Reacher", "3")])
b = myx_classes.Book()
b.setSeriesFromLog("Jack Reacher", "Jack Reacher #3") # '#' with the names present
self.assertEqual([(s.name, s.part) for s in b.series], [("Jack Reacher", "3")])
b = myx_classes.Book()
b.setSeriesFromLog("Jack Reacher", "") # names only
self.assertEqual([(s.name, s.part) for s in b.series], [("Jack Reacher", "")])
b = myx_classes.Book()
b.setSeriesFromLog("", "")
self.assertEqual(b.series, [])
Loading