Conversation
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
Releasing the write.lock part way through the retry budget raced with Lucene 3's own lock bookkeeping: on Windows a competing Obtain left a handle on the file, so Release and the temp directory cleanup both failed with a sharing violation. Hold the lock for the whole budget instead and assert what actually matters, that a lock conflict is retried and leaves the index files alone. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
Windows CI caught real over-reach. When another holder has write.lock, Windows raises the sharing violation before Lucene can turn it into a LockObtainFailedException, so it arrives as a plain IOException. Repairing anything that is not a recognised lock conflict then meant deleting the index the other holder was using -- exactly the second-instance case the retry exists for. An IOException naming Lucene's write lock now counts as a lock conflict. Matching the file name rather than the message wording keeps it working on non-English Windows. The end-to-end test asserts the property instead of the exception type, since the type legitimately differs by platform. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
The two path assertions added with the PDF fix compared a path the test built itself against one that had been through LongPath, which on Windows prefixes a drive-rooted path with \\?\ so paths past the 260 character limit work. Linux adds no prefix, so this only showed up on the Windows job. Normalising both sides is not just about the false failure. The inequality assertion guarding 'the PDF was saved loose in the Books directory' compared a raw temp path against a prefixed one, so on Windows it passed on the prefix alone and would not have caught the bug it exists to catch. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
Both grids restored the last good filter by recursing into the filter handler, which never terminated once the search index rather than the query was the problem: the restore fails the same way, and the retry uses the same filter. The user got an endless run of dialogs, each of them blaming a filter string that was fine. Only an empty last-good filter broke the loop, because that short-circuits before reaching the search engine. The fallback is now a bounded sequence -- last good filter, then no filter -- and the message distinguishes an index Libation cannot reach from a query it cannot parse. Only the first failure is reported, so restoring is quiet. A malformed query never surfaces as an IO-family exception, which QueryFailureShapeTests pins against the real engine, so a typo is never mistaken for index trouble or made to trigger a rebuild. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
This reverts 6daaf33. Master is 13.7.8 and the next release is the 0.0.1 increment from it, so the original 13.7.9 references were correct. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
…ry-backoff-0844 Fix rmcrackan#1947: refused licenses re-requested every run, missing and misplaced PDFs, unbounded log
The dialog names the account in full because it is shown to whoever owns it, but log files get attached to public issue reports, which is why the codebase has MaskedLogEntry. Naming the account in the log the same way the dialog does would have put real email addresses into every shared log. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
…lucene-search-index-3c5b Heal a search index Lucene cannot open, and say which account needs a login
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
…9-bf7c Bump version to 13.7.9
…l downloads Snapshot GetVisible() on the UI thread before handing off to Task.Run, preventing InvalidOperationException when parallel downloads modify the collection concurrently. Also fix installer script to use 64-bit dotnet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… of the PR Restores the plain 'dotnet publish' invocations in Scripts/Windows/Build-WindowsInstaller.ps1. Hardcoding C:\Program Files\dotnet\dotnet.exe is machine-specific and unrelated to parallel downloads.
ClearQueue() only stops new work from starting. With parallel downloads, books already running kept going after an abort or a disk-full result, so the queue reported itself stopped while downloads continued. Adds ProcessQueueViewModel.CancelAllAsync(), which clears the queue and cancels every active book, and uses it for both the abort and disk-full paths. Both Cancel All buttons now route through it as well - the Avalonia one previously cancelled only Queue.Current, so it missed every book but the first.
…d a numeric control Replaces the MultiThreadEnabled checkbox and the ProcessorCount default with a single MaxConcurrentDownloads value where 1 means serial - the behaviour Libation had before parallel downloads existed. The old pairing could not express 'off' at all: the bool was hardcoded true in the view model constructor and the int setter clamped to a minimum of 2, so there was no way back to one-at-a-time downloads. Collapsing both into one value makes that state unreachable rather than merely fixed, and removes the risk of the two settings disagreeing. Bounds live in Configuration as named constants: minimum 1, default 3, maximum 10. Audible throttles license requests, so the default is deliberately conservative and the cap keeps users from choosing a number that produces license denials instead of speed. WinForms swaps the 'Parallel downloads' checkbox for an 'At once:' spinner that reads its bounds from those constants.
…sor count Chardonnay had parallel downloads with no way to configure them, since the queue logic lives in shared UI code but each UI supplies its own controls. Adds the Auto-scroll toggle and the 'At once' spinner to Chardonnay's queue panel, bound to the same view model properties the WinForms panel uses. Also uses Environment.ProcessorCount as the spinner's ceiling rather than its default: min(ProcessorCount, 10). Downloading is bound by Audible's license throttling rather than local CPU, so core count says nothing about how many concurrent downloads will succeed - it only bounds how many decrypts can usefully run at once. The default stays 3. Spinner bounds are bound rather than hardcoded, so the two UIs cannot drift apart.
The 'At once' spinner sat at x136-181 on the second row, but the 'DL Limit:' label starts at x148. Moves the label and spinner up to the first row beside the Auto-scroll checkbox, where there is clear space between Cancel All and Clear Finished.
The queue loop only woke when a book finished, so books queued a moment after it started sat idle until an in-flight download happened to complete. Observed as one book downloading alone for 15 seconds with a limit of 5, then four starting within 37ms of the first finishing. The loop now waits on whichever comes first: an active task completing, or a new book being queued. The enqueue signal is captured before the queue is inspected, so a book queued between TryDequeueNext and the wait completes the token already held rather than a fresh one - otherwise the wakeup is lost and the loop sleeps until something else finishes. The 'nothing left, exit' path re-checks the same signal for the same reason.
Both of these are reachable only once more than one book is active at a time, and both were found by compiling TrackedQueue<T> on its own. MarkCompleted moved a book from Active to Completed and raised only CompletedCountChanged. The display index of an active book is derived from Completed.Count, so when the second of two active books finishes first the two swap places - with no CollectionChanged, a bound list keeps painting the old order and rows show the wrong book's progress. Out-of-order completion is the normal case with parallel downloads. Now raises a Move for the book whose position actually changed; with one book at a time the indices are equal and nothing is raised, so the sequential path behaves exactly as before. GetAllItems built a lazy Concat inside the lock and returned it, so the enumeration ran unlocked. Any foreach or LINQ over the queue while a book task mutated it threw InvalidOperationException. It now returns a snapshot taken while the lock is held. This is the source of the crash that setLiberatedVisibleMenuItem was patched for at the symptom. RemoveActive and ClearCurrent also read _completed.Count outside the lock when computing the index to report; both now capture it inside.
The opt-in daily download limit landed after this branch was written and lives inside the sequential while (Queue.MoveNext()) loop this change replaces. It is sequential by construction, so rebasing alone leaves it subtly wrong rather than merely conflicted. The gate now runs in the dispatch loop, between taking a book off the queue and starting its task. That keeps the existing semantics - checked as a book is about to start so the queue keeps its contents and the limit can be changed mid-run - while books already in flight carry on. Putting it inside the book task instead would have every blocked book polling the history at once. Three concrete defects that fell out of the collision: RequeueLast deferred a book with Queue.ClearCurrent(), which drops Active[0]. With one book at a time that is the book being deferred; with three in flight it is somebody else's download, so deferring the second active book silently evicted the first. It now removes the book it was given. A book cancelled at the gate was left on the active list. The sequential loop retired it on the next MoveNext(); there is no next MoveNext(), so it is marked completed explicitly. CancelAllAsync existed twice after the rebase - the sequential version that sets cancelAllRequested and cancels Queue.Current, and this branch's version that cancels every active book. Unified into one that does both. The flag matters: a queue paused on the limit is sitting in WaitForDailyLimitAsync and that flag is how it learns to stop. AnyOtherQueuedBookAllowed enumerates the queue with Queue.Any(...) while book tasks mutate it; that is safe now that GetAllItems snapshots under the lock, and it also now takes a copy of the active list to cancel.
Faulted book tasks were dropped unobserved. The reaping pass removes any completed task from the active set before the closing WhenAll can rethrow, so an exception out of ProcessOneAsync - which can happen via GetFailureActionAsync in its finally - went nowhere. In the sequential loop it reached the outer catch and was logged. It is logged again. The bad-book dialog did not survive concurrency. The license and Widevine messages are guarded to appear once per run, but the "skip this book?" dialog is per book and its "apply to all remaining books" answer lands in a shared BadBookSessionContext. Three books failing together put three modals on screen racing to set the same override. The dialog is now serialised on the session, and each book re-checks the override after its turn comes: if the book ahead answered "apply to all", the question is not asked again. Machine capability no longer overwrites the stored concurrency setting. MaxAllowedConcurrentDownloads clamped both the getter and the setter, so a user who chose 8 and then opened the same config on a two-core machine read back 2 - and had 2 written over their 8. The stored value is now bounded only by the hard limit, which is machine-independent; processor count bounds the spinner and is applied at the point of use, where the loop decides how many books to run.
Same unlocked-enumeration problem as the others: the speed limit is changed from the UI thread while book tasks are starting and finishing, and Active is the live list.
TrackedQueue<T> is a pure data structure with no dependencies beyond System, so the behaviour parallel downloads relies on can be asserted directly rather than inferred from the UI. Every case here needs more than one book active at once, which is what made these defects unreachable before. The two that mattered: a book finishing out of order now reports the reorder, so a list bound only to CollectionChanged stays in step with the queue; and the queue can be enumerated while it is being mutated, which used to throw. The sequential path is covered too - one book at a time still raises no Move, because the book that finishes is already first.
Bounding the persisted setting by processor count was only half of it. Both spinners take their maximum from the same machine-derived number and are two-way, so a NumericUpDown handed a value above its maximum coerces the display down and writes the coerced value straight back. A user who chose 8 on their desktop and opened the queue panel on a two-core laptop still ended up with 8 replaced by 2 - the same defect, one layer up, and not fixed by the configuration change alone. The bound is now the machine's capability or the stored value, whichever is higher, so the spinner can never coerce what is already there. Lowering it remains the user's to do, and what actually runs is still held down to what the machine can manage at the point of use.
<title short> stops at the first colon, so it shortens Audible titles that contain one just as readily as it drops Audible's subtitle, and distinct books then collapse onto the same name. A colon cannot be searched for: the analyzer discards punctuation and Lucene reads a colon in a query as a field separator. Two bool index fields find the affected books instead. Document how the two title tags differ, since <audible title> already drops Audible's subtitle without ever cutting a title, and how to audit for names that actually collide in a spreadsheet export. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
A storefront that no longer lists a title still answers a request for it: HTTP 200, total_results 1, and a product carrying an asin and a few always-returned flags. Nothing between the request and the file noticed, so a re-download replaced a metadata file written while the title was still listed - the only copy of that data - with the placeholder. Fetch the product before touching the destination, and leave the file alone when the product carries no title. Reported in issue rmcrackan#1947, where a Canada-only title produced {"asin":...,"asset_details":[],"is_preview_enabled":false,"is_vvab":false,"rating":{...}} against every other storefront. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
That the file existed said only that the server sent a body, and an Audible error is a 200 with a JSON body like any other response. Dinah's downloader renames by Content-Disposition, so such a body landed in the book's folder under whatever Audible called it and the title was recorded as having its PDF. Check the payload: a file named .pdf must carry the PDF header, and nothing may begin with the opening character of a JSON or markup document. A rejected download is deleted rather than left in the library, which also lets the empty-folder cleanup run, and the file is added to the path cache only once it has passed. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
…nly in AGENTS.md Prompt audit 2026-09-02: replace the history narrative with the rule and its reason, and describe global.json as a 10.0.101 floor with latestFeature roll-forward rather than a pin. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rom, not only the account's home store. Co-authored-by: Cursor <cursoragent@cursor.com>
…ght to CreateAsync. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…oad-store-locale License extra-marketplace titles against the store they were scanned from
CustomerThrottled was treated as a generic License Denied, so non-Plus titles got no explanation. Co-authored-by: Cursor <cursoragent@cursor.com>
…cense-ui Cursor/throttling license UI
Co-authored-by: Cursor <cursoragent@cursor.com>
…series-number Do not use Audible's sentinel episode numbers as podcast series order.
Owner
Author
|
Fork-only S9 evidence collection is complete. Exact source d78737f passed validation run 33826389731 across the full Windows, macOS, and Linux package matrix; Windows/Linux capture artifacts were downloaded and inspected. Closing this temporary CI vehicle without merge, publication, distribution, or upstream contact. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Temporary fork-only CI vehicle for S9. Do not merge. Runs the existing supported-platform validation/package matrix and exports Windows/Linux headless captures for inspection. This is not an upstream PR, publication, notarization, rollout, or release.