Releases: jlafuenti/Book-Sync
Release list
Tandem 0.7.0
Added
-
The home page's "Continue Series" row is now Next up (#716), with the same rule as the
Android app's new Next up row: the next book in each series you have started or finished a book
of in the last 90 days. The old row only counted a series while one of its books was in progress,
so finishing book 3 without starting book 4 dropped the series instead of suggesting book 4; and it
had no time limit, so a series opened once long ago stayed forever (measured on a real library, one
reader's list went from 26 series to 4 with the 90-day limit). The next book is the lowest-numbered
one above the furthest you have read, novellas numbered like 3.5 included; a pair counts once; a
book merely opened at 0 % does not count as started. A card now opens that book's details page
rather than the Series page. Both clients are held to the same test cases
(server/tests/fixtures/sync_parity/next_up_cases.json). -
The Account page links to the project's community Discord (#714), where testers ask questions,
report bugs and suggest features; the Android app's Account screen has the same link. It opens in
a new tab. The invite is a permanent one, also listed under Contact in the privacy policy. -
The series name on a book's details page opens that series (#717). It was plain text, so
reaching the next book meant going back and finding the series by hand. The name now opens the
Library filtered to that series, on the web (/library?series=<name>, the same place the Series
page's own cards lead) and in the Android app, so the series' books are listed straight away; the
book number stays plain text. The filter is an exact name match, so a pair whose ebook and
audiobook spell the series differently can be missing from the list opened from one of them.
Fixed
- A paired book could open at the beginning after being read or listened to (#720). A position
saved to a paired book's own ebook or audiobook, rather than to the pair, was stored as a separate
record beside the pair's, and opening the pair only reads the pair's record; when that separate
record was the only one, the book opened at the start. Such a write now lands on the pair: it is
judged against the pair's newest position like any other, and the response names the pair.
Reading or resetting a paired book's own ebook or audiobook is unchanged, so resetting one half
still leaves the pair's position alone.
Upgrade notes
- Migration
0026_fold_orphan_positionsmoves each position that was a reader's only record for a
paired book onto the pair, keeping its place, device and timestamps; the book then opens where it
was left. It changes no schema and has no downgrade step. Positions that sit beside a pair
position are left as they are, as is a position that also refers to a book outside the pair (what
unpairing leaves behind). On the instance this was measured on, 23 positions across three
readers moved.
Tandem 0.6.0
Added
- Troubleshoot now lists possible duplicates as a report-only category alongside the existing
exact-hashDuplicate filescheck (#692). The exact-hash check only ever catches byte-identical
files, and a real duplicate almost never is one: a remux, a re-tag, or Tandem's own EPUB
metadata write-back (#533) changes every byte while leaving the same work behind, so genuine
duplicates accumulated invisibly. The new category matches audiobooks by duration and ebooks by
file size, but neither signal is trusted alone — on a library of any size, some pairs of
genuinely different books turn up within a few seconds or the same byte size purely by chance.
Every candidate also needs a second, independent signal already on the row before it is
reported: for ebooks, a matching title or overlapping author is enough on top of the exact size
match — that combination is a rare coincidence. For audiobooks, a matching title or a shared
ASIN/ISBN is required to qualify a duration match; a shared author or narrator alone is not
enough, even together, because one author's backlist and one narrator's whole catalogue
routinely share similar running times — that was tried first and produced exactly that false
positive against a real library, which is why the rule is what it is now. Author and narrator
matches still show up as supporting evidence once a title or identifier has already qualified
the pair. When one copy of a candidate pair is paired to a book and the other is not, the
unpaired copy is marked as the likely one to remove. Nothing here is deleted automatically or
offered a delete/bulk-delete action — a false candidate is expected, not exceptional (two
different books can be the same size), so the operator judges each one from the evidence in its
detail.
Fixed
-
Accent-coloured text, focus rings and active borders are readable in the default Blueprint
theme (#705). Blueprint's violet (#7c3aed) was used for them directly, and on its own dark
surfaces it measured 2.5–3.3:1: under the 4.5:1 WCAG AA asks of text, and on cards even under
the 3:1 it asks of focus rings and other state indicators. That covered the active sidebar link,
hovered and active filter pills, the active bottom-nav tab, links and focused inputs. A new
theme token,--accent-ink, now draws the accent on a dark surface: Blueprint uses its lighter
secondary violet (#a78bfa, 5.2:1 or better on every surface), and the other four themes keep
their own accent, which already passed. Filled buttons and badges keep the darker accent, whose
labels #694 fixed. Trade-off: in Blueprint, that text and those borders are now a lighter
violet than the filled buttons. A test fails the build if the raw accent is used as text or as a
line again. -
The web player no longer shows 0:00, or saves a position of 0, while an audiobook is still
loading (#697). Between opening a book and the audio being ready, which took over 20 seconds
on a slow connection, the player sat at0:00 / -0:00with play enabled. Pressing play there
rewound from 0, and the next pause saved position 0 as an audiobook-sourced write, which on a
paired book also moved the ebook back to the start. Until the saved position has been applied,
the player now shows it (and the book's length) with a spinner, and play, skip and the seek bar
are disabled; playback starts by itself at the saved position once the audio is ready, as it
always did. A tab close in that window saves the saved position rather than 0, and a second
book opened before the first finished loading no longer inherits the first book's start
position. Trade-off: on a slow link you cannot start, skip or scrub until the audio has loaded.
Positions already overwritten by the old behaviour are not restored. -
Troubleshoot's file-level items now say which pair a file belongs to (#700). Every file-level
category (missing, too small, corrupt audio, DRM, unreadable ebook, bad chapter titles,
unsupported format, duplicates, possible duplicates, missing cover) reportedpair_id: null
even for a paired file, so a corrupt audiobook read as an unpaired, low-stakes file when its
pair was in fact queued for transcription, about to spend hours of worker time on audio that is
mostly missing. Those items now carry the file'spair_id. For the categories that make a
transcript useless (missing, too small, corrupt audio, DRM, unreadable ebook), the detail also
ends with "paired, queued for transcription" or "paired, being transcribed now" when the pair
has a job waiting or running, so the job can be pulled first. Nothing is stored; the page
computes this on every load, at the cost of one extra query against the queue. -
Labels on accent-coloured buttons and badges are readable in every theme (#694). They were
hard-coded white, which only suits Blueprint's dark violet. On the light accents of Forest
Night, Ember, Aurora and Slate, white measured about 2.1–2.4:1 against the fill, well under the
4.5:1 that WCAG AA asks of normal text. Affected: primary buttons, the sidebar avatar, the player's
play and speed buttons, active filter and sort pills, count badges, the mobile upload button and
the placeholder cover's initial. A new theme token,--on-accent, now sets that ink: white on
Blueprint (unchanged) and the theme's own dark background on the other four (5.5:1 or better on
both the accent and its hover shade). Those labels are now dark rather than white in the four
light-accent themes. A test fails the build if white text reappears on an accent fill. -
A pair's plausibility verdict now records when it was last checked (#699). Rewriting an
existinglibrary_check_resultsrow updated the verdict but leftchecked_atat the time the
row was first created, so a pair that "Library verify" had just re-checked and cleared (#693)
still showed a timestamp from days earlier, as if the re-check had never run. Every write now
stampschecked_at. Rows already stored keep their old time until the next Library verify
re-checks them; only pairs currently flagged are re-checked, so a passing pair's time still
reflects when it was paired. -
A passing words-per-hour plausibility check could still be overruled by the imprecise
bytes-per-hour check, permanently flagging heavily illustrated ebooks (picture books, comics,
cookbooks) asimplausible_paireven though their audio narrates them at a completely normal
pace (#693).check_pair_plausibilitynow treats a passing word-count verdict as final and
skips the byte check entirely when a word count is available; the byte check only ever runs on
its own when no word count could be computed for the ebook, and its message now says so
explicitly. Trade-off: a pair with a plausible word count is no longer checked by file size at
all — the word count is trusted outright as the more precise signal, so it can no longer act as
a second line of defense behind a coincidentally plausible-looking word count. Since that verdict
is only ever recorded at pair creation, fixing the check alone could not clear a false positive
already stored on a running server, so "Library verify" (Troubleshoot → Verify library) now also
re-checks any pair whose stored verdict currently says implausible, and clears it if a freshly
computed word count now passes; a pair whose stored verdict is already plausible is left alone.
The added scan phase also exposed a small pre-existing display bug: the Troubleshoot page's
progress bar guessed the total phase count before the server had reported one, so it briefly
showed "phase 1 of 3" and then flipped once the real total came back. It no longer guesses — the
phase total only renders once the server has actually reported it. -
A pair is now strictly one-to-one: an ebook can be in at most one
BookPair, and an audiobook
can be in at most one. Before,book_pairswas only unique on the combination of the two ids,
so the same ebook could end up paired to two different audiobooks at once — once by an
auto-match during a scan, once by hand through the Pairs page — with nothing objecting. Each
pair queued its own transcription job for the same book, and because reading position keys on
book_pair_id, a reader's progress could silently split across the two pairs depending on which
one they happened to open.POST /api/library/pairsnow returns 409 naming the existing pair
when the ebook or the audiobook is already paired,auto_matchalready skipped a paired book on
its own and now cannot double-claim one within a single scan either, and the EPUB-conversion
relink path refuses (rather than silently dropping a pair) if its target already has one. The
trade-off: you can no longer keep two recordings of one book (e.g. a straight reading and a
dramatization) paired at the same time — unpair one first (#691). -
The web reader left open in a tab now catches up with listening done elsewhere (#683, the web
half of #682). Before, it kept showing the page from before, because it only worked out where to
open when the book was first opened, and the first page turn from that stale page was saved as
a reading position over the real, later listening position. Now, when the tab becomes visible
again, the reader holds its saves, re-reads the position, and, if the audiobook moved on by 30
seconds or more since the reader last knew, re-opens the book there the same way a fresh open
would. Costs one position read each time the tab comes back. A page turned in the moment
between returning to the tab and that read coming back is not saved.
Upgrade notes
-
Run Library verify (Troubleshoot → Verify library) after deploying this change to clear pairs
that were flagged by file size despite a normal word count (#693). -
Migration
0025_book_pairs_one_to_onereplacesbook_pairs'uq_book_pairs_pairconstraint
with ...
Tandem 0.5.1
[0.5.1] - 2026-09-21
Changed
- Dependency updates since 0.5.0: the Android reader moves to Readium 3.4 (a reading position saved
under 3.3 was checked to restore to the identical locator), Media3 1.11.1 and the Android Gradle
plugin 9.4.1; uvicorn 0.53 and alembic 1.20 on the server; and 12 further minor/patch updates
across the server, web, Jetson and CI actions (#663, #664, #665, #666, #667, #668).
Fixed
- Realigning a book no longer makes it look recently read. Clients rank "last read" by a
position'scaptured_atand fall back toupdated_atwhen it is empty, and a realign's
bookmark remap bumpsupdated_at. Most positions written beforecaptured_atexisted have it
empty, so every realigned book jumped to the front of Continue Reading and the phone fetched
its sync map. Migration0024_captured_at_backfillcopiesupdated_atinto an empty
captured_atonbookmarksanduser_progress; the remap pins the oldupdated_atinto
captured_atif it finds one still empty; and the web's Continue Reading now sorts by
captured_at, notupdated_at(#679). - The ebook could reopen on the page from before a period of listening — reported from a car,
where Android Auto resumed the audiobook and disconnecting paused it, and the ebook then opened
on the pre-drive page instead of where listening had stopped. A locator hint captured while
reading, with noaudio_position_msstamped beside it, skipped the drift check that would
otherwise have dropped it, and a drive that stayed within one chapter never bumped
anchor_revisioneither — so the stale hint won the restore, online, offline, and for a reader
left open across the trip. The restore ladder (server, web, Android) now treats a hint with no
audio anchor as unusable whenever its record is audiobook-sourced, falling through to the audio
rung instead. Android also re-anchors a reader that returns from the background once the
canonical record's audio has moved on past a threshold, and no longer lets a page whose
sync-point lookup missed inherit an older page's audio time (#682).
Upgrade notes
- Migration
0024_captured_at_backfillis a one-time data backfill and runs automatically on
start. Its downgrade does nothing: the copied values stay, and they are the values clients were
already using. Books realigned before this deploy keep the realign time as their last-read time.
Tandem 0.5.0
Added
GET /api/sync/positions: every position the caller has (pair, standalone ebook, standalone
audiobook), paged (page/limit, default 100, cap 500, same convention as the library list
endpoints), in the same shapeGET /api/sync/position/{scope}/{ident}already returns —
reusesto_response_dictso the two cannot drift. Read-only;PUT /api/sync/position/{scope}/{ident}
remains the only write path. Exists because a fresh sign-in on Android pulled positions with
oneGET /position/{scope}/{ident}per pair bookmark plus one per paired audiobook, sequentially
— several hundred round trips against the single-process server on a library of a few hundred
pairs, with Home's Continue Reading filling in over minutes. Bounded query cost regardless of
page size (one count, one page of rows, one apiece for the two eager-loaded relationships).
Android'sPositionRepository.syncAllBookmarksAndProgressnow pulls this list once at sign-in
instead of looping per pair, falling back to the old per-pair loop when the server predates this
endpoint (#653).
Fixed
-
Sync maps no longer drift by hours through the middle of a book. The aligner pins its map to
"anchors" — sentences found exactly once in the audio — and aligns the text between them. Its
anchor search fuzzy-scored each EPUB sentence against individual transcript sentences with a
scorer that rates a short fragment ~100 against any long sentence containing its words, and
narration is full of fragments like "He said." Two of them far apart always tied, so nearly every
candidate was thrown out as "ambiguous": a 31-hour book kept 2 anchors, and whole books were
aligned by proportion between three fixed points. Measured against the real transcripts, maps
were off by up to +115 min (a book where every chapter is narrated, #650) and −155 min
(books whose EPUB carries back matter the audiobook never reads, #648). The search now locates a
slice of each sentence in the whole transcript and anchors only where it occurs exactly once,
and searches every sentence near the start and end so anchoring runs right up to where the
narration stops. On the same books it keeps hundreds of anchors, every scored chapter lands
within a minute of the narration, and aligning is roughly 40× faster. Realign was previously
deterministic and reproduced the broken map byte for byte; it now produces a correct one (#648,
#650). -
The SSRF guard no longer resolves DNS on the event loop.
assert_safe_urllooked the hostname
up with a synchronoussocket.getaddrinfo(), and threeasync defhandlers called it directly —
cover fetching during matching (reachable by editors, and run once per redirect hop) and the
System page's Audiobookshelf and transcription-worker connection tests. One uvicorn worker serves
every request, so for the length of each lookup nothing else was served: position sync,
streaming, login and/api/healthall stalled. Invisible with a resolver that answers in
milliseconds; seconds long with one that has to time out, and the symptom — unrelated endpoints
timing out — never pointed at the cause. Those callers now useassert_safe_url_async, which
crosses to a worker thread once. What the guard accepts and rejects is unchanged. A source-guard
test fails the build if anyasync defcalls the synchronous guard again (#673). -
A server restart no longer makes an in-progress remote transcription look like it restarted
from zero.queue_manager.reset_stale_items()used to re-stampstarted_atto the restart
moment and hand every recovered row back to the pipeline exactly like a brand-new item — which
re-ran the integrity gates (decoding the whole audio file again on the server, for no reason)
and briefly publishedprogress≈0.01/0.02with an early-pipeline message before the worker's
first post-restart status poll corrected it. None of that was true: the remote worker keeps
running across a server restart and loses nothing (confirmed in production across four restarts
of the same 13.5-hour job, which went on to complete normally) — only the server's picture of
the job was wrong, and it was wrong in the one direction that looks like six hours of GPU time
destroyed.reset_stale_items()now leavesstarted_atalone and flags the row instead
(_recovered_at_startup), so_process_next_itemtreats it like a resume — skipping the
integrity gates and the startup-sequence progress numbers, and writing "Reattaching to
transcription after restart..." rather than a message that implies either a restart or a
checkpointed resume, neither of which is what happened. A genuinely new or genuinely failed item
is unaffected: the flag is one-shot, consumed the moment the row is claimed (#661). -
A position write that moves
epub_chapterwithout carrying anepub_sentence_indexnow clears
the stored index (and thesync_map_versionattesting to it) instead of leaving the previous
chapter's index beside the new chapter. A sentence index is a coordinate within a chapter, so
a chapter change invalidates it; the stored pair is the portable cross-device anchor every client
restores from, and a mismatched pair resolved to a position the reader was never at — confidently,
because nothing marked it as suspect. Android's reader sends exactly this shape whenever its
sync-point lookup misses (#644 fixed the client's own local row; this is the canonical record's
half, and it also covers app builds predating that fix, which keep sending this shape for as long
as they are installed). Deliberately narrow: only a write that states a different chapter clears
the index. A write carrying no chapter at all — an audio heartbeat, a completion toggle — still
leaves every anchor alone, which is what stops background saves eating a real position. Clearing
the index moves the anchor, so hints captured against the old one correctly stop being served as
current (#658).
Upgrade notes
-
Sync maps built before this release keep their drift until they are realigned — the fix is in
how a map is built, not in the maps already stored. After deploying, realign the affected pairs;
the sync-map audit's timing check flags them. Realigning bumps the map's version, which marks
readers' saved positions for re-anchoring against the new map (#55, #116), so pairs with active
readers are worth doing deliberately rather than in one sweep. Pairs aligned for the first time
after deploying need nothing (#648, #650). -
The bulk positions endpoint (#653) needs no action. An app build that predates this release keeps
working against a server that has it (it simply never calls the new endpoint), and an app build that has it falls back to the
old per-pair loop against a server that predates it.
Android app
Android changes are not recorded in this changelog (Play carries their notes); the app in this release is versionCode 500.
- Android Auto and the player pick up where you stopped reading, not where you last listened (#643).
- Sync data for books you've opened downloads in the background, with new per-device settings in Account → Storage, including Wi-Fi only (#655).
- Signing in pulls reading positions in one request instead of two per book, when the server supports it (#653).
- Reading positions stay consistent across chapter changes and devices (#617, #644, #656).
- The guided walkthrough, and the library now loading at sign-in (#646, #647, #651, #654).
Tandem 0.4.2
Fixed
_filter_chunked_segment_drift(0.4.0, #620) demoted nearly every matched point in a large
anchor-bounded gap instead of just the genuinely displaced ones, making realign strictly worse
than doing nothing on exactly the pairs it was meant to repair — production re-realigns of the
12 affected pairs all came back with fewer matched points and more audit timing mismatches
than before (one pair: 12,268 matched -> 252; 8/74 mismatches -> 62/74). Root cause: the filter
judges every point in a segment against one straight line drawn between its two bracketing
anchors, but that line is only accurate when the epub:whisper density is uniform across the
whole segment — and a segment large enough to need chunked alignment in the first place (up
to a whole book, on the sparse-anchor fallback) is exactly where it is least likely to be. An
ordinary pacing difference between two large stretches of a real book was then enough to put
correct matches on the far side of the difference outside tolerance for the rest of the
segment, and because they were all off in the same direction the run-length check did nothing
to contain it.CHUNK_DRIFT_MAX_DEMOTION_FRACTION(30%) bounds the damage: if applying the
filter would demote more than that share of a segment's matches, the reference itself is the
more likely thing that's wrong, and the segment's chunked output is now left untouched instead
(#635).- Android: Report a problem attaches the Android Auto diagnostics log as well as the app's,
when each exists, and the message names which logs it carries — plus how to switch on
whichever is missing. A report about the car used to arrive with none of the evidence about
the car (#636).
Upgrade notes
- The 12 pairs from #635's production re-audit (and any others the sync-map audit flags
status: "realign") need to be realigned again after this deploys — the 0.4.1 realign that hit
this bug left them with fewer matched sync points than before, and only a fresh realign under
the fix restores them. - This narrows what #620's chunked-drift fix (0.4.0) actually corrects: a genuinely displaced run
that is a small minority of a large gap's points is still caught and repaired, but a gap whose
matches are mostly correct and simply spread across a non-uniform pace is now left alone
rather than being "corrected" into a straight-line interpolation that #635 showed is typically a
worse estimate than the original chunked output. A smarter, locally-scoped reference (instead of
one chord per segment) would let the filter safely re-cover that case; not attempted here in the
interest of shipping the regression fix first.
Tandem 0.4.1
Android only. Nothing in the server or the web app changed since 0.4.0, so a deployment has
nothing to do. Android normally carries its notes in Play rather than here; this one is listed
because it repairs something 0.4.0 shipped broken.
Fixed
- The reader's Define worked for no word at all in 0.4.0. The dictionary lookup moved to
Wiktionary in that release, but the app identified itself to Wikimedia only by the HTTP
library's default name, which Wikimedia's robot policy refuses with 403 — and the fallback to
the previous dictionary ran only when a word was genuinely absent, so every lookup ended as
"No definition available". The app now identifies itself properly (its name, version, site and
support address — nothing about the device or the reader), and any failure of the first source
falls back to the second. A lookup reports failure only when both sources fail.
Tandem 0.4.0
[0.4.0] - 2026-09-17
Added
implausible_pair(GET /api/troubleshoot/library) now also checks words-per-hour, not just
bytes-per-hour: a real unabridged reading lands around 8,000-12,000 words/hour, and file size
alone can miss an abridgement or excerpt whose EPUB happens to carry heavy images/fonts (two
real pairs — an abridgement at ~29.3k words/h and a one-hour excerpt at ~80.8k words/h — slipped
past the byte-based check for exactly that reason) (#620). The same check now runs before
auto_match_bookscreates a pairing, not only after — a word-rate-implausible pair is no longer
auto-matched (and, with auto-transcribe on, queued) in the first place; a manual pairing still
only warns, unchanged from before.
Changed
- Android app 0.4.0 (versionCode 400) ships a guided walkthrough: a five-minute tour over the
real screens — Home, a book's menu and details, the sync map, the reader and the player
(including hopping between them at a sentence), Library, Downloaded and Account — where the
user taps the real controls, can quit at any time, and can replay it from Account → Help. It
is offered once after the first sign-in. The book it opens is put back the way it was when
the tour ends. The reader's selection-toolbar "Sync to Audio" now also works while
streaming, not only with a downloaded audiobook (#597). - Widened the rule that un-finishes a book when its position moves back out of the end stretch
(#584): it used to clearis_completedonly on the transition out of the end zone, so a book
already sitting mid-book when it was marked finished — for example, re-listened from the middle
on a build that predated that rule — had both its before and after positions outside the zone on
the next write, and could never un-finish. Now any write that moves the stored position (the
new value differs from what's stored) to somewhere outside the end zone clears the flag,
regardless of which side of the boundary the previous position was already on. A write that
doesn't move the position — a heartbeat, or a resend of the same value — still never clears it,
which is what keeps a manual "mark finished" sticking. Applies to both the server rule (every
client inherits it) and Android's local mirror for standalone books (#613). - Alignment's degraded-map classifier (#586/#595) no longer fires on a tiny anchor pool: with very
few anchors surviving the outlier filter (2-25 raw, seen in production), the local trend a
rejected run is judged against is too sparse to trust, and a small book's ordinary fuzzy-match
noise could look exactly like a genuine reordered block.MIN_KEPT_ANCHORS_FOR_DEGRADED(20)
gates the fraction/run rule on enough surviving anchors first; the sync-map audit
(GET /api/troubleshoot/sync-map-audit) separately lets a clean, well-sampled timing check
(timing_status == "ok"over 20+ checked points) override a leftover alignment-time flag instead
of compounding with it (#620). - Alignment could drift smoothly for hundreds of sentences (up to hours, on a real book) and then
recover, inside a single large anchor-bounded gap whose local epub:whisper sentence-count ratio
varies (footnotes, chapter headings or other content present in the epub but never spoken,
clustered unevenly) —_chunk_align's fixed-size windows assume one ratio for the whole gap, and
a wrong-sized window can force a confident match to the wrong audio, with the error carrying
into the next window until the true content reappears in range._filter_chunked_segment_drift
checks chunked output against the gap's own two bracketing anchors and demotes a sustained
displaced run back to unmatched so it re-interpolates instead (#620).
Android artifacts. tandem-0.4.0-versionCode400.aab is the bundle published to Google Play (internal testing) and tandem-0.4.0-versionCode400.apk is the same build for sideloading. Both are signed with the Play upload key, so a sideloaded install cannot update to the Play-installed one or vice versa — pick one path per device.
Tandem 0.3.0
Added
- The sync-map audit (
GET /api/troubleshoot/sync-map-audit) checks timings as well as text. For a
sample of 80 sync points it finds each sentence in the cached transcript and compares
timestamps, and flags the pair when at least 10% of the located points are more than 2 minutes
off and at least 3 of those are consecutive. That is the signature of an audiobook whose
narration is reordered relative to the ebook, which the hash and text checks never caught.
Flagged pairs reportstatus: "degraded"with the suggested actioncheck_audio_order:
re-aligning cannot fix reordered audio, the file has to be corrected first. The web Troubleshoot
page has a new on-demand "Sync-Map Audit" section (#586). - Alignment marks a sync map as degraded (
sync_maps.degraded,degraded_reason) when the anchor
filter throws away a large, contiguous, consistently displaced run of matches — a reordered
block that used to be papered over with interpolated timestamps. Scattered rejections from
ordinary noise do not trip it (#586).
Changed
- A finished book is un-finished when its position moves back out of the end stretch — the mirror
of the rule that finishes it on entering. Re-listening or re-reading from the middle used to
leave the book off every Continue Listening / Continue Reading list for good. Rewinding from the
final minute to replay the ending now also un-finishes the book until it reaches the end again.
The server applies the rule for every client; Android also applies it locally to standalone
books so they reappear straight away (#584). - The sync-map audit tells a map problem from an audio problem. When the cached transcript is in
book order, a flagged pair now getssuggested_action: "realign"instead of
check_audio_order, and the reason names the affected chapters.check_audio_orderstays for
transcripts that really are out of order, or when there is no transcript to check (#595).
Fixed
-
The transcript cache no longer trusts the audiobook's path alone. A transcript now records the
fingerprint (file hash and duration) of the audio it was made from, and is reused only when that
still matches. Tandem's own tag write-backs move the fingerprint forward, so they never look like
a new file (#588). -
A library scan, rescan-all or single rescan notices when the file at a known audiobook path has
changed. A duration change of more than 2 seconds is treated as a replaced recording: the cached
transcript is dropped and a synced pair goes back tomanual_matched, exactly as
POST /api/troubleshoot/replace/audiobook/{id}already did. A changed hash with the same
duration is treated as a tag edit made outside Tandem (Audiobookshelf, a tag editor): the stored
hash and size are refreshed and the transcript is kept. Reading positions are untouched either
way (#588). -
A transcript with no fingerprint yet is checked against the file's current duration before it is
reused; one that ends well short of the file or runs past its end is re-transcribed instead of
being trusted (#588). -
Android: Define and Sync to Audio read the selection from the page on screen. After moving to an
adjacent chapter they used to read a neighbouring chapter's page, giving "Select a word to
define", a definition for an earlier word, or a "select more text" error. Switch to Audio reads
the visible chapter for the same reason. Curly apostrophes, possessives and invisible characters
are cleaned out of a word before it is looked up (#582). -
Android Auto's Continue Listening and Library refresh while they are on screen, instead of
showing a stale list until the app is reopened (#583). -
Alignment no longer lets one wrong match far ahead survive and drag the surrounding stretch of
the map toward it — the "exact, then hours off, decaying back to exact" shape. Each kept match
is judged against the trend its neighbours imply and outliers are dropped. On a real affected
22-hour book the drift of up to 4 hours disappeared and more sentences matched directly, at
about 1.3x the alignment time (#595). -
The sync-map audit flags maps whose chapter order disagrees with the EPUB's spine — maps built
when chapters were ordered by file name (part1, part10, part2 …) — and suggests realigning
them (#595). -
Android: a guided walkthrough introduces every screen, spotlighting the real control and having
you tap through the app yourself, including the hop from a sentence to the matching moment in
the audiobook. It can be quit at any time and replayed from Account (#597).
Upgrade notes
- Two migrations run on start-up.
0022_sync_map_degradedaddssync_maps.degraded(existing
maps default to false) anddegraded_reason.0023_audio_fingerprintadds the transcript
fingerprint columns, empty for existing transcripts; they are filled in the next time each
transcript is used, so nothing is re-transcribed on deploy. - Existing sync maps are not re-examined automatically. Run Sync-Map Audit on the Troubleshoot
page once. Pairs markedrealignare fixed with Realign from their cached transcript, with
no re-transcription; onlycheck_audio_orderneeds the audio file itself looked at. Existing
maps are not realigned automatically.
Tandem 0.2.3
Android only. Nothing in the server or the web app changed, so a deployment has nothing to do —
this release exists to ship the Android build. Android normally carries its notes in Play rather
than here (see the note at the top of this file); they are listed because a version with an empty
section reads like an oversight.
Fixed
- Android Auto refuses to browse, search or play anything while no account is signed in. The
browse tree is built from a local cache that signing out deliberately keeps, so a head unit went
on listing the previous account's library and playing its downloaded books (#573). - The local library cache is cleared when it belongs to a different server, including the
downloaded files and cached covers, which were keyed by id alone and could show the wrong book's
artwork after a switch. An account change on the same server keeps both, because a Tandem
server's library is shared by its users (#575). - Android Auto's Continue Listening is ordered by when each book was last played, across paired
and standalone books, and the cap is applied after that ordering rather than before. The voice
query with no title shared the bug and is fixed with it (#574). - The phone's own media controls show cover art again: the artwork is published as image data
rather than a file link the system UI has no permission to open (#570).
Tandem 0.2.2
Fixed
- A cleared or changed audiobook description now sticks through the next library scan for every
format. Clearing an M4B's description also removes its©desatom, which the scanner reads
first, and a new description is written there too, so a stale©desno longer outranks it.
Clearing a FLAC or Ogg description also removessummary, and a new MP3 description replaces
any comment frame a ripper left behind rather than sitting beside it (#538). - The book being transcribed appears once, not twice, under In Progress on the phone-width
Transcription page, and its card shows the queue's live progress (#562). - ACSM imports of Adobe-DRM books that also carry obfuscated fonts work again. The DeDRM plugin
release the image pins (10.0.3) crashes on those books — it rewritesMETA-INF/encryption.xml
for the leftover font entries from a value it never stored — and the import failed with a bare
"ACSM conversion failed (exit 6)". The DRM image build now applies upstream's two-line fix, the
same way it already patches DeACSM, and fails the build if the patch stops applying. A
decryption failure also reports itself as one, naming the exception, instead of reading like an
expired loan or a missing Adobe authorization (#566). - EPUBs whose chapters are XHTML files without an
.xhtmlor.htmlname extract their text. The
server picked content documents by file extension, but EPUB identifies them by the manifest
media-type, and some publishers name themchapter01.xmlor give them no extension at all. A
book built that way throughout extracted no text, failed transcription at the integrity check and
was listed under DRM in Troubleshoot; a book built that way in part got a sync map missing those
chapters. Chapters that were already read keep their chapter and sentence numbers (#561). - EPUBs that only obfuscate their embedded fonts are no longer reported as DRM-encrypted or refused
for transcription. EveryMETA-INF/encryption.xmluses the XML encryption vocabulary, and the
check treated any such file as Adobe ADEPT, but font obfuscation leaves the text readable in
every reader. Calibre writes these files when it converts a book with embedded fonts, so books
from Tandem's own Convert flow were affected. A book is now DRM-encrypted only when the manifest
encrypts something other than fonts. The ACSM import's post-decryption check uses the same
rule (#560).
Upgrade notes
- Migration
0020deletes the cached "ebook produced almost no text" integrity failures again: a
Library verify run after 0.2.1 re-cached them for books this bug still broke. A pair already
synced from an EPUB that was only partly affected is missing those chapters until you realign it. - Migration
0021deletes the cached "EPUB is DRM-encrypted" integrity failures; books that really
are encrypted fail again when re-checked. - After upgrading, run Library verify once, then retry any transcription that was refused as
DRM-encrypted or for "almost no text". - The DeDRM fix lives in the image, so a deployment that imports ACSM files must rebuild with
docker compose build --build-arg INSTALL_DRM_PLUGINS=1rather than only restarting; pulling a
published image is enough only if it was built with that flag. Re-try any ACSM loan that failed
with "ACSM conversion failed (exit 6)". The default image is unchanged: it still ships no DRM
tooling.