Skip to content

Releases: jlafuenti/Book-Sync

Tandem 0.7.0

Choose a tag to compare

@jlafuenti jlafuenti released this 26 Sep 14:09
7a22565

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_positions moves 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

Choose a tag to compare

@jlafuenti jlafuenti released this 24 Sep 00:40
e4c6f93

Added

  • Troubleshoot now lists possible duplicates as a report-only category alongside the existing
    exact-hash Duplicate files check (#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 at 0:00 / -0:00 with 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) reported pair_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's pair_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
    existing library_check_results row updated the verdict but left checked_at at 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
    stamps checked_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) as implausible_pair even though their audio narrates them at a completely normal
    pace (#693). check_pair_plausibility now 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_pairs was 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/pairs now returns 409 naming the existing pair
    when the ebook or the audiobook is already paired, auto_match already 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_one replaces book_pairs' uq_book_pairs_pair constraint
    with ...

Read more

Tandem 0.5.1

Choose a tag to compare

@jlafuenti jlafuenti released this 21 Sep 20:28
8ddf054

[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's captured_at and fall back to updated_at when it is empty, and a realign's
    bookmark remap bumps updated_at. Most positions written before captured_at existed have it
    empty, so every realigned book jumped to the front of Continue Reading and the phone fetched
    its sync map. Migration 0024_captured_at_backfill copies updated_at into an empty
    captured_at on bookmarks and user_progress; the remap pins the old updated_at into
    captured_at if it finds one still empty; and the web's Continue Reading now sorts by
    captured_at, not updated_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 no audio_position_ms stamped beside it, skipped the drift check that would
    otherwise have dropped it, and a drive that stayed within one chapter never bumped
    anchor_revision either — 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_backfill is 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

Choose a tag to compare

@jlafuenti jlafuenti released this 21 Sep 13:02
1529877

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 shape GET /api/sync/position/{scope}/{ident} already returns —
    reuses to_response_dict so 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
    one GET /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's PositionRepository.syncAllBookmarksAndProgress now 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_url looked the hostname
    up with a synchronous socket.getaddrinfo(), and three async def handlers 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/health all 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 use assert_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 any async def calls 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-stamp started_at to 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 published progress≈0.01/0.02 with 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 leaves started_at alone and flags the row instead
    (_recovered_at_startup), so _process_next_item treats 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_chapter without carrying an epub_sentence_index now clears
    the stored index (and the sync_map_version attesting 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

Choose a tag to compare

@jlafuenti jlafuenti released this 18 Sep 17:55
faec6be

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

Choose a tag to compare

@jlafuenti jlafuenti released this 18 Sep 11:29
bf41a33

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

Choose a tag to compare

@jlafuenti jlafuenti released this 18 Sep 10:21
86dcd0f

[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_books creates 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 clear is_completed only 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

Choose a tag to compare

@jlafuenti jlafuenti released this 17 Sep 20:31
d7c4b99

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 report status: "degraded" with the suggested action check_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 gets suggested_action: "realign" instead of
    check_audio_order, and the reason names the affected chapters. check_audio_order stays 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 to manual_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_degraded adds sync_maps.degraded (existing
    maps default to false) and degraded_reason. 0023_audio_fingerprint adds 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 marked realign are fixed with Realign from their cached transcript, with
    no re-transcription; only check_audio_order needs the audio file itself looked at. Existing
    maps are not realigned automatically.

Tandem 0.2.3

Choose a tag to compare

@jlafuenti jlafuenti released this 16 Sep 04:27
acd0ef5

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

Choose a tag to compare

@jlafuenti jlafuenti released this 16 Sep 00:27
75f6306

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 ©des atom, which the scanner reads
    first, and a new description is written there too, so a stale ©des no longer outranks it.
    Clearing a FLAC or Ogg description also removes summary, 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 rewrites META-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 .xhtml or .html name extract their text. The
    server picked content documents by file extension, but EPUB identifies them by the manifest
    media-type, and some publishers name them chapter01.xml or 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. Every META-INF/encryption.xml uses 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 0020 deletes 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 0021 deletes 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=1 rather 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.

Android (not tracked in the changelog; Play carries its own notes)

  • Android Auto's Continue Listening lists books you are streaming, not only downloaded ones (#569).
  • Covers show in Home's In Queue row (#549) and for standalone ebooks across the app (#550).