Commit Graph
150 Commits
Author SHA1 Message Date
Cursor Agentandrmcrackan a0ef5c3d95 fix: repair a search index that is missing books
A book the index does not hold cannot be found by any positive filter, and
every negated filter - which resolves to every document in the index - drops
it from the grid instead. That is why issue #1989 reads as half a working
filter: Absent found nothing while -Absent removed exactly the absent books.

The index is only ever written as a whole, and a rebuild that fails is
deliberately swallowed so a bad index cannot fail a good scan, so a short
index stayed short until something else changed the library. Count the
index against the library once per run and rebuild when it is short.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-24 18:00:52 +00:00
Cursor Agentandrmcrackan 1e81c86f65 Merge remote-tracking branch 'origin/master' into cursor/fix-pdf-download-retry-storm-a274
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 19:31:14 +00:00
Cursor Agentandrmcrackan 6a166b23fb Merge master
LibationFileManager.csproj conflicted because both sides edited adjacent
PackageReference lines: this branch bumped AudibleApi, master's #1974 bumped
Microsoft.Extensions.Configuration.Json. Kept both.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 18:55:42 +00:00
Cursor Agentandrmcrackan 71e8d8eea1 Adopt Dinah.Core 10.2.5.1 and AudibleApi 11.0.4.1
Picks up the .NET 10.0.11 dependency floors both now declare.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 18:51:50 +00:00
Cursor Agentandrmcrackan f0d344099a fix(scan): keep a book's supplement in step with what the last scan says
Only a newly imported book ever recorded a supplement, so a title that gained a
PDF after its first import never got one, and a title that lost its PDF went on
claiming one - which in issue #1973 is why three titles Audible has no PDF for
were still being asked for.

Sync from updateBook as well, and give Book set-semantics for the one supplement
Audible reports per title. The duplicate guard compared the incoming url to
itself, so it happened to mean 'this book already has a supplement' and a url
that had changed was silently ignored.

A supplement is dropped only when Audible says outright that no supplement url is
available. A missing url says nothing by itself: episodes come from the catalog,
which is never asked for pdf_url, so there it means 'not asked'. A PDF already
downloaded is left alone either way, since the file is on disk and the library
should go on saying so.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 18:36:02 +00:00
Cursor Agentandrmcrackan 4a138bb42f Move the .NET 10 package references to 10.0.11
Microsoft.EntityFrameworkCore.Sqlite 10.0.11 is the first 10.0.x servicing
release to depend on SQLitePCLRaw 2.1.12, which drops the pre-3.50.2 SQLite
build flagged by GHSA-2m69-gcr7-jv3q (CVE-2025-6965). DataLayer's Sqlite
reference is where that chain enters the solution, so bumping it clears
NU1903 from all 23 projects that transitively saw it.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 17:55:55 +00:00
Cursor Agentandrmcrackan e6e8ba73e0 Upgrade to Dinah.Core 10.2.4.1 and AudibleApi 11.0.3.1
Seven references across six projects, all now on published versions.

What the app gains is from Dinah.Core: OsSecretStore.Create bounds how
long it waits for the backend, so a Linux or macOS start with a keyring
that never answers falls through to the portable master key path instead
of hanging there. IdentityTokenStorageWiring.ResolveSecretStore is the
caller, and it runs at startup whenever no key file or env var is set.

AudibleApi 11.0.3.1 carries no code change for us - it is the release
where its nuspec finally declares the Dinah.Core floor its own code needs.

Dinah.Core.WindowsDesktop and Dinah.EntityFrameworkCore move to 10.2.4.1
as well, keeping every Dinah package on one version.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 17:53:53 +00:00
Cursor Agentandrmcrackan 2b3325ed0d Point at Dinah.Core 10.2.3.1
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 02:31:49 +00:00
Cursor Agentandrmcrackan c813d6ee98 Drop AsScalar; SecretString reports its own shape now
Dinah.Core 10.2.2.2 gives SecretString a Redacted property, so a
destructured secret carries its length without the logger being told
anything. That was the only thing AsScalar was buying - safety never
depended on it - so the registration and its explanation go away, and the
knowledge lives in the type instead of in this file.

MaskedLogEntryPolicy stays: an ILogMasked has no equivalent property, and
without the policy one logged as {@Account} is still written out property
by property.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 02:26:46 +00:00
Cursor Agentandrmcrackan 337cb3169a Adopt Dinah.Core 10.2.2.1 and AudibleApi 11.0.0.1
AudibleApi 11 holds token, key, and cookie values in a SecretString
rather than a string, so nothing public exposes plaintext for a reflective
logger to find. Picking it up is a breaking upgrade: the seven package
references move, and the nine places that read a secret now call Reveal().

Two of those needed thought rather than a mechanical edit. Mkb79Auth
exports to and imports from audible-cli's JSON format, which is plaintext
by definition, so the cookie projections reveal explicitly in both
directions and the file format is unchanged. And the account's own
DecryptKey stays a plain string here: converting it is separate work.

This is the dependency bump only. The log leak it enables fixing - an
AuthenticationRequiredException carrying a live Account, whose address and
activation bytes Serilog.Exceptions writes into a shared log - is still
open, and none of the account-side masking has landed yet.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 01:49:21 +00:00
Cursor Agentandrmcrackan 54485c0825 fix(pdf): save a PDF with its book instead of loose in the Books directory
getProposedDownloadFilePath looked for the book's audio file and fell back to
the Books directory itself when it found none. That lookup matches on the
product id appearing in the path, so it finds nothing for a library whose
folder and file templates omit <id>, and nothing for a title marked downloaded
whose files are not on this machine. Those PDFs landed in the library root,
where they also shared one namespace and so could collide with each other.

Fall back to the folder template instead - the same folder the audiobook itself
would go in - and create it, since nothing else does on the PDF-only path.

Also give MockLibraryBook a three-field version: ToVersionString formats to at
least three fields, so the two-field default threw as soon as anything rendered
a naming template for a mock book.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-16 17:01:29 +00:00
Cursor Agentandrmcrackan 824ff10dd9 fix(download): stop asking Audible for a license it just refused
A license denial left no trace: BookStatus stayed NotLiberated, so every
liberate run asked again. Only the GUI's bad-book dialog could mark a title
Error, and license denials take their own path and never reach that dialog, so
a headless install had no way at all to stop the retries. A cron schedule then
re-requested the same refused licenses every run and printed the same warning
block for each one, which is both wasted API traffic and the log noise reported
in issue #1947.

Record the refusal instead, with a wait that doubles per consecutive failure:
one day for an eligibility denial (up to 30), six hours for a title Audible has
no audio for yet such as an unreleased preorder (up to 7 days), one hour when
the denial names GenericError, which the GUI already reads as an outage
(up to 12). Nothing is permanent - every kind is attempted again on its own.

Only failures attributable to Audible are recorded. A dropped connection, a
decrypt error or a full disk keeps being retried on the next run as before.

Naming a title, --force, and setting a download status all clear the record:
asking for a title explicitly overrides the wait.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-16 16:13:17 +00:00
Cursor Agentandrmcrackan 818510d1b0 feat(limit): add opt-in daily download limit core, history store and enforcement
Records every successful audiobook download in the library database (a new
DownloadHistory table) and, when the user opts in, stops downloading once the
rolling 24 hour window is full.

The history lives in the database rather than a file under LibationFiles
because in Docker only the database is on a volume; a file there is discarded
on every container restart.

The limit is checked immediately before each book downloads, never at queueing
time, so a full queue stays full and the user can raise or disable the limit
mid-run. When nothing in the queue can proceed the queue pauses and re-checks
every 15 seconds, recomputing settings, history and clock from scratch, so a
queue left running for days drip-feeds itself as downloads age out. The CLI
never waits: it skips covered titles and reports a count.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-14 17:16:47 +00:00
Cursor Agentandrmcrackan 4e6bba54b5 bugfix #1925: show orphaned episodes as standalone rows
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-10 03:20:07 +00:00
Robert McRackan 5f4a75aade Detect identity decrypt failures by exception type 2026-08-01 11:25:56 -04:00
Robert McRackan 20150cc4ed update dependencies 2026-07-25 22:03:23 -04:00
Robert McRackan a9e6f75443 update dependencies 2026-07-25 20:57:52 -04:00
Robert McRackan b5485f77bf update dependencies 2026-07-25 20:08:11 -04:00
rmcrackan b654a8b551 #1867 - Fix duplicate-ASIN crashes. New Hangover recovery 2026-06-12 11:48:32 -04:00
Matthew Alvernaz 63325b9cad Skip EF migration lock when no migrations are pending (#1729)
DbContexts.GetContext() runs ApplyMigrations on every context creation,
calling Database.Migrate(). Migrate() acquires the EF migration lock
before checking whether anything is pending. For SQLite that lock is a
persisted row in __EFMigrationsLock with no timeout (SQLite has no
connection-scoped lock that frees on disconnect). A process killed after
acquiring the lock but before releasing it - even during an otherwise
no-op Migrate() - orphans the row, and every later Migrate() then spins
forever in SqliteHistoryRepository.AcquireDatabaseLock(). The row lives
in the db file, so restarts never clear it, matching the "scan hangs,
reboot doesn't help" reports in #1729.

Guard Migrate()/MigrateAsync() behind GetPendingMigrations(), which only
reads __EFMigrationsHistory and never acquires the lock. The steady-state
path (schema already current) no longer touches the lock at all; first
run and real migrations are unchanged.
2026-06-06 22:23:12 -07:00
Michael Bucari-Tovo df531de255 Update Avalonia to v12
Avalonia 12 uses compiled bindings by default. Converted all remaining reflection bindings into compiled bindings,

Fixed binding errors
2026-05-04 21:08:26 -06:00
rmcrackan dd059a0ce9 #1776 - Make Linux Snap SQLite startup failures easier to understand and fix (stale LibationFiles in appsettings.json after a revision change), improve crash and log detail (inner exceptions + paths), and document that in the docs and FAQ. Tests lock in exception formatting behavior. 2026-05-01 10:01:03 -04:00
rmcrackan 13bdebbe9d #1776. better messages to users. improve all exception display 2026-04-30 11:04:50 -04:00
Michael Bucari-Tovo b8616a6da0 Re-add Category Name Column to database
Category names were accidently removed during the last DB migration in d67692355f.

Re-add the Names column and modify CategoryImporter to upsert the names on update.
2026-04-27 14:45:13 -06:00
Jo-Be-Co b6c7612ec4 Merge remote-tracking branch 'upstream/master' into language_and_region 2026-04-14 00:55:42 +02:00
Jo-Be-Co b4f48cb54c Extend <language> and <locale> tags to allow different outputs like ISO-codes or names in different languages 2026-04-10 09:43:11 +02:00
rmcrackan 26946e8a87 Log initial db state, esp to capture books in trash 2026-04-09 10:57:41 -04:00
rmcrackan ccef50d681 move new check into datalayer 2026-04-08 17:43:21 -04:00
Michael Bucari-Tovo 3ab1edc076 Code Cleanup
Make fields readonly
Remove unnecessary casts
Format document
Remove unnecessary usings
Sort usings
Use file-level namespaces
Order modifiers
2026-02-05 12:48:44 -07:00
Michael Bucari-Tovo d67692355f Enable project-wide nullable reference types
Added DB migration for nullable types.
2026-02-05 12:43:05 -07:00
Michael Bucari-Tovo 29501bddf3 Clean up queries
Use .NET 10 extensions
Refactor some extensions for clarity
2026-01-22 13:36:25 -07:00
Michael Bucari-Tovo f54a789ae8 Improve querying UnLiberated books
Add GetUnliberated_Flat_NoTracking() which queries only unliberated books/episodes and does not load the entire library.

Fix UnLiberated() query to only return products or episodes (not parents) (#1564)
2026-01-19 16:57:32 -07:00
Michael Bucari-Tovo c1298e9ff6 Update dependencies 2026-01-13 12:02:36 -07:00
Michael Bucari-Tovo 79e2bca8fc Update dependencies 2026-01-12 10:42:04 -07:00
Michael Bucari-Tovo 659f793eb8 Improve TrashBinDialog functionality
- Use the main display grid control to display deleted books
- Added search functionality for deleted books. This required creating a temporary search index in the `InProgress` folder. The products grid control now uses an instance of `ISearchEngine` to filter its grid entries.  The main grid uses a singleton instance of `MainSearchEngine`, which merely wraps `SearchEngineCommands.Search()`.  The TrashBinDialogs use `TempSearchEngine`.
- Users can now batch select `Everyting` as well as `Audible Plus Books`

Avalonia:
  - Refactor main grid context menus to no longer require reflection
2026-01-09 16:47:37 -07:00
Michael Bucari-Tovo 804bac5c4c Add LibraryBook.IsAudiblePlus property 2026-01-07 15:50:23 -07:00
MBucari e850465ec1 Add more null safety
Enable project-wide nullable on LibationUiBase and LibationAvalonia

Explicitly parallelize unit tests
2025-12-30 13:17:11 -07:00
Mbucari 31087c0855 Add feature to scan library for higher quality books 2025-12-29 19:30:47 -07:00
MBucari fa30c10435 Fix PDF validation error (#1470 ) 2025-12-01 20:39:52 -07:00
Mbucari b9a1709284 Use release version of EntityFrameworkCore.PostgreSQL 2025-11-22 11:46:27 -07:00
Michael Bucari-Tovo b0a40e12b7 Create some extension members
Trying out .NET 10s extension members with some Book extension properties.
2025-11-21 12:31:50 -07:00
MBucari fde78f4167 Update to .NET 10
- Update all project runtime targets
- Update all dependencies
  - NOTE: Using Npgsql.EntityFrameworkCore.PostgreSQL RTM build from MyGet
- Delete unused pubxml files (they were made redundant by recent workflow changes)
- Replace Libation.sln with Libation.slnx
2025-11-20 15:56:47 -07:00
MBucari ce2b81036f Add license and settings overrides to LibationCli
- Add `LIBATION_FILES_DIR` environment variable to specify LibationFiles directory instead of appsettings.json
- OptionsBase supports overriding setting
  - Added `EphemeralSettings` which are loaded from Settings.json once and can be modified with the `--override` command parameter
- Added `get-setting` command
  - Prints (editable) settings and their values. Prints specified settings, or all settings if none specified
  - `--listEnumValues` option will list all names for a speficied enum-type settings. If no setting names are specified, prints all enum values for all enum settings.
  - Prints in a text-based table or bare with `-b` switch
- Added `get-license` command which requests a content license and prints it as a json to stdout
- Improved `liberate` command
  - Added `-force` option to force liberation without validation.
  - Added support to download with a license file supplied to stdin
  - Improve startup performance when downloading explicit ASIN(s)
  - Fix long-standing bug where cover art was not being downloading
2025-11-19 23:47:41 -07:00
Michael Bucari-Tovo 69a8eaad4a Add mock LibraryBook and Configuration capabilities
- Added `MockLibraryBook` which contains factories for easily creating mock LibraryBooks and Books
- Added mock Configuration
  - New `IPersistentDictionary` interface
  - New `MockPersistentDictionary` class which uses a `JObject` as its data store
  - Added `public static Configuration CreateMockInstance()`
    - This method returns a mock Configuration instance **and also sets the `Configuration.Instance` property**
    - Throws an exception if not in debug
- Updated all chardonnay controls to use the mocks in design mode. Previously I was using my actual database and settings file, but that approach is fragile and is unfriendly towards anyone else trying to work on it.
2025-11-05 13:28:49 -07:00
Michael Bucari-Tovo f98adef9e9 Update Dependencies
- Remove package references that are already included transitively
- Change Avalonia.ReactiveUI to ReactiveUI.Avalonia
2025-11-03 09:34:23 -07:00
Robert 0c512162ab Reverting migration notes from #1402 2025-11-03 10:21:13 -05:00
Taylor Southwick 1b5db9b28f Add support for postgres
Supporting postgres simplifies deployments to environments such as kubernetes. Since sqlite doesn't work well on nfs shares it can be easier for databases to have a dedicated db set up that applications can connect to. Sqlite is easier for most deployments though, so this will default to that if the settings haven't been updated to support it.

This change does the following:

- Separate out SQLite from the DataLayer and adds a Postgres assembly for migrations as well
- Add a configuration setting for a postgres connection string that will be used if it is there, otherwise reverts to the original sqlite string
- Add a copydb command for the cli to bootstrap the postgres db
- A convenience script to update migrations for both dbs at the same time
2025-10-27 16:30:50 -07:00
Michael Bucari-Tovo afffeb953c Enforce sequential access to DbContext. 2025-10-22 09:22:05 -06:00
delebash f7cd2b106b fix migration files 2025-10-20 14:12:04 -04:00
delebash 07e51f2191 IncludedUntil migration 2025-10-20 13:09:41 -04:00