Compare commits

..
2180 Commits
Author SHA1 Message Date
rmcrackan 7bd14bd5af Merge pull request #1870 from rmcrackan/rmcrackan/incr-ver
incr ver
2026-06-12 11:54:35 -04:00
rmcrackan a85179f50a incr ver 2026-06-12 11:54:14 -04:00
rmcrackan 158ee3d5ba Merge pull request #1869 from rmcrackan/rmcrackan/1867-duplicate-asin
#1867 - Fix duplicate-ASIN crashes. New Hangover recovery
2026-06-12 11:52:42 -04:00
rmcrackan b654a8b551 #1867 - Fix duplicate-ASIN crashes. New Hangover recovery 2026-06-12 11:48:32 -04:00
rmcrackan bc28398e1c Merge pull request #1868 from rmcrackan/rmcrackan/1865-bad-book-setting
#1865 - Add in-dialog "Apply to all" and "Remember in Settings"
2026-06-12 10:10:29 -04:00
rmcrackan 4d1b78f7e4 #1865 - Add in-dialog "Apply to all" and "Remember in Settings" for bad-book errors. Also a testing section when run in debug (ie: from visual studio) 2026-06-12 09:53:57 -04:00
rmcrackan a50712256c Merge pull request #1864 from rmcrackan/rmcrackan/1862-security
Rmcrackan/1862 security
2026-06-10 14:31:47 -04:00
rmcrackan 4345971e81 escape string to avoid accidental json injection 2026-06-10 14:29:57 -04:00
rmcrackan 1ad458fe5b CSPRNG-safe 2026-06-10 12:12:38 -04:00
Robert 1547e6bec3 incr ver 2026-06-08 15:41:20 -04:00
rmcrackan 81175e5427 The edit targets two problems in the macOS bundle script.
Reliability: `hdiutil create` can fail on GitHub’s macOS runners with `Resource busy` even when the app build is fine. The retry wrapper gives it several attempts, waits between tries, and cleans up partial DMGs so transient runner issues are less likely to break the release.

Correctness: The script used to keep going after `hdiutil` failed and still exit successfully, which left `./bundle/` empty and made artifact upload fail with a misleading error. `set -euo pipefail` and explicit failure exits make a DMG creation failure stop the job immediately instead of pretending the bundle step succeeded.
2026-06-08 14:42:05 -04:00
rmcrackan 14551c745f Merge pull request #1861 from rmcrackan/rmcrackan/elevate-upgrader
Only elevate upgrader when necessary
2026-06-08 14:31:09 -04:00
rmcrackan e3cfe3a8f0 Only elevate upgrader when necessary 2026-06-08 14:30:35 -04:00
rmcrackan 51fbe73ea1 make vitepress happy 2026-06-08 14:23:00 -04:00
rmcrackan 41fa7b7142 Merge pull request #1860 from rmcrackan/rmcrackan/win-installer-phase-1
Finish windows installers, phase 1
2026-06-08 14:19:41 -04:00
rmcrackan b7ce96e91d Finish windows installers, phase 1 2026-06-08 14:03:59 -04:00
rmcrackan a25fd35f29 Fix release upload path for merge-multiple artifact download 2026-06-08 13:19:10 -04:00
rmcrackan 20a99c6e20 Exclude Docker cache blobs from release artifact download 2026-06-08 11:59:03 -04:00
rmcrackan f39cfaccc7 Fix release artifact pattern for Libation-Classic assets 2026-06-08 11:21:20 -04:00
rmcrackan 6db78e4268 Merge pull request #1859 from rmcrackan/rmcrackan/windows-installer-fix
Fix arm64 Windows zip artifact name in CI matrix
2026-06-08 11:03:49 -04:00
rmcrackan 74e9cf24a8 Fix arm64 Windows zip artifact name in CI matrix 2026-06-08 11:01:42 -04:00
rmcrackan 6ce88072c5 Merge pull request #1858 from rmcrackan/rmcrackan/windows-installer-fix
yaml bug fix
2026-06-08 10:53:50 -04:00
rmcrackan 9058c7368c yaml bug fix 2026-06-08 10:53:05 -04:00
rmcrackan 37d5517756 Merge pull request #1857 from rmcrackan/rmcrackan/windows-installer
Add Windows installers to release build
2026-06-08 10:45:39 -04:00
rmcrackan 0a33863635 Merge branch 'master' into rmcrackan/windows-installer 2026-06-08 10:44:58 -04:00
rmcrackan d03955a2ca Add Windows installers to release build 2026-06-08 10:43:32 -04:00
Robert b2c5ae3c2e incr ver 2026-06-08 10:27:17 -04:00
rmcrackan fb7e0857c6 Merge pull request #1856 from rmcrackan/rmcrackan/releaseindex
update backwards compat releaseindex in prep for future changes
2026-06-08 10:15:26 -04:00
rmcrackan 60965902ca update backwards compat releaseindex in prep for future changes 2026-06-08 10:15:00 -04:00
rmcrackan ef62ee15e2 Merge branch 'master' of https://github.com/rmcrackan/Libation 2026-06-07 12:56:21 -04:00
rmcrackan c285522a0c Add contributor from PR #1854 2026-06-07 12:56:13 -04:00
rmcrackan 0fb3eb152f Merge pull request #1854 from matalvernaz/fix/1729-sqlite-migration-lock-deadlock
Skip EF migration lock when no migrations are pending (fixes SQLite scan-hang #1729)
2026-06-07 12:55:20 -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
rmcrackan 37a60ca654 Merge pull request #1853 from rmcrackan/rmcrackan-patch-1
increm ver
2026-06-05 10:42:50 -04:00
rmcrackan 6623300883 Update AppScaffolding.csproj 2026-06-05 10:42:21 -04:00
rmcrackan 329df0882a Merge pull request #1852 from rmcrackan/rmcrackan/1851-threading
#1851 - threading bug
2026-06-04 10:23:11 -04:00
rmcrackan da000694d7 upgrade dependency 2026-06-04 10:17:23 -04:00
rmcrackan f1a7a837f3 CI bug. try upgrading 2026-06-04 10:13:22 -04:00
rmcrackan 869917e0ce #1851 - threading bug 2026-06-04 09:59:12 -04:00
rmcrackan d532e40fe9 Merge pull request #1850 from rmcrackan/rmcrackan/doc-cleanup
Clean up docs
2026-06-03 11:05:49 -04:00
rmcrackan 9ef5e95801 Clean up docs 2026-06-03 11:04:59 -04:00
rmcrackan c1c3c33ea8 Merge pull request #1849 from rmcrackan/rmcrackan/docs-better-atmos
better atmos docs
2026-06-02 16:45:36 -04:00
rmcrackan b31e7cb819 better atmos docs 2026-06-02 16:44:54 -04:00
rmcrackan 314a46d59d Merge pull request #1848 from rmcrackan/rmcrackan/docs-spacial-audio
new page for spacial audio info
2026-06-02 16:39:21 -04:00
rmcrackan 721f09a62b new page for spacial audio info 2026-06-02 16:38:12 -04:00
rmcrackan 545b3057d4 Merge pull request #1847 from rmcrackan/rmcrackan/1845-better-non-json
#1845 - improve error message for common VPN blocking errors
2026-06-02 12:58:30 -04:00
rmcrackan 2c2ae2c8b0 #1845 - improve error message for common VPN blocking errors. improve troubleshooting docs 2026-06-02 12:53:20 -04:00
rmcrackan f456c68176 Merge pull request #1846 from rmcrackan/rmcrackan/1844-nre
#1844 - add lock to avoid race/NRE
2026-06-02 10:59:25 -04:00
rmcrackan e6c17053f7 #1844 - add lock to avoid race/NRE 2026-06-02 10:54:14 -04:00
rmcrackan 0c22b821bf incr ver 2026-05-25 11:16:38 -04:00
rmcrackan 8902ddc565 Merge pull request #1838 from rmcrackan/rmcrackan/1836-disk-space
#1836 - Fix misleading "Not enough disk space" warnings
2026-05-25 11:15:08 -04:00
rmcrackan 9ed98cbfff unit test fix: this is always going be a weird test. it's trying to balance machine speed vs long running process -- and test 'unreasonable' response time 2026-05-25 10:58:57 -04:00
rmcrackan 3cf5d0b3fa unit test bug fix: culture info 2026-05-25 10:46:34 -04:00
rmcrackan 6bd1ea7ca4 fix unit tests for windows-only logic 2026-05-25 10:41:20 -04:00
rmcrackan f8c5f0da68 #1836 - Fix misleading "Not enough disk space" warnings when Books and In progress are on different drives 2026-05-25 10:25:46 -04:00
rmcrackan a3f272a48e #1835 - add log file guidance to FAQ 2026-05-22 15:00:34 -04:00
Robert 9ee59738eb incr ver 2026-05-21 10:36:21 -04:00
rmcrackan f57839c104 Merge pull request #1834 from rmcrackan/rmcrackan/1833-pdf-bug-fix
Download works but queue goes red with Download PDF
2026-05-21 10:33:52 -04:00
rmcrackan d63fabd73d Download works but queue goes red with Download PDF 2026-05-21 10:33:13 -04:00
rmcrackan 54a97cc3c4 Merge pull request #1830 from rmcrackan/fix/docs-vite-security-override
Pin vite 6.4.2 for docs site (Dependabot GHSA-4w7w-66w2-5vf9)
2026-05-19 09:44:28 -04:00
rmcrackanandCursor 1e3daa40dc Pin vite 6.4.2 for docs site to resolve Dependabot alert.
VitePress 1.6.4 requires vite ^5.4.14, which has no backport for
GHSA-4w7w-66w2-5vf9. An npm override keeps VitePress 1.x while
using a patched vite. Deploy workflow now watches package files.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 09:40:44 -04:00
rmcrackan 8f3f069d54 Merge pull request #1829 from rmcrackan/rmcrakan/13-4-2
incr ver
2026-05-19 08:57:00 -04:00
rmcrackan 88e3bbab89 Merge pull request #1828 from rmcrackan/rmcrackan/1827-cli-liberate-single
#1827 - CLI option to liberate single book
2026-05-19 08:56:49 -04:00
rmcrackan ca52ed4b7a incr ver 2026-05-19 08:55:57 -04:00
rmcrackan bb4a5298ae #1827 - CLI option to liberate single book 2026-05-19 08:51:21 -04:00
rmcrackan 9512e62deb Merge pull request #1824 from rmcrackan/rmcrackan/1819-ntfs-note
#1819 - Better NTFS invalid character note
2026-05-18 16:47:25 -04:00
rmcrackan 91174ed8dd Merge pull request #1826 from rmcrackan/rmcrackan/1823-full-disk
#1823 - Address full disk better
2026-05-18 10:09:33 -04:00
rmcrackan b9490966bb * Multi-step backup: PDF and later steps now run in the same awaited chain, so disk full on PDF stops the queue instead of counting as success.
* Detection: Quota / NAS “out of space” messages are treated like disk full, not generic retries.
2026-05-18 10:00:32 -04:00
rmcrackan d1114b410e sync/async bug fixes 2026-05-18 09:53:05 -04:00
rmcrackan 49e9b0c9ce oops, wrong branch. revert 2026-05-18 09:42:08 -04:00
rmcrackan 0b0d1afc48 Fix build failures on non-windows 2026-05-18 09:39:57 -04:00
rmcrackan 77f518f8d3 #1823 - Address full disk better. Fail clearly and early when possible, fail safely when space cannot be queried, and do not take down the UI or spam the user when the disk is full. 2026-05-18 09:20:51 -04:00
rmcrackan 9f1d4a383b Merge pull request #1825 from rmcrackan/rmcrackan/1822-crash-after-upgrade
#1822 - Fix the startup crash after auto-upgrade
2026-05-18 09:02:42 -04:00
rmcrackan 74b18c170a #1822 - Fix the startup crash after auto-upgrade by loading the library only after InteropFactory assembly resolution is ready, checking that Microsoft.EntityFrameworkCore.Sqlite.dll is present in the install folder, and showing a clear reinstall message instead of a generic "Unexpected error" when that still fails. 2026-05-18 08:54:40 -04:00
rmcrackan c85c11b27a #1819 - Better NTFS invalid character note 2026-05-18 08:21:51 -04:00
rmcrackan c8ac585ab3 Merge pull request #1817 from rmcrackan/rmcrackan/1811-threading
#1811 - fix UI threading
2026-05-15 12:10:39 -04:00
rmcrackan 55b45ae45d #1811 - fix UI threading 2026-05-15 12:02:23 -04:00
rmcrackan 776cd5215f Merge pull request #1816 from rmcrackan/rmcrackan/docker-yaml-context
Avoid default git context refs/pull/*/merge (breaks after PR merge or…
2026-05-15 11:45:03 -04:00
rmcrackan 1b8840c3b0 Avoid default git context refs/pull/*/merge (breaks after PR merge or on some re-runs) 2026-05-15 11:13:04 -04:00
rmcrackan 97537df933 Merge pull request #1815 from rmcrackan/rmcrackan/update-contribs
Add @CryptoJones to contributor list
2026-05-15 09:41:54 -04:00
rmcrackan eb4ab59737 Add @CryptoJones to contributor list 2026-05-15 09:41:33 -04:00
rmcrackan 55c2bf3be7 Merge pull request #1814 from CryptoJones/fix/tag-culture-test-de-ch-locale
Fix Tag_culture_test platform dependency on de-CH NumberGroupSeparator
2026-05-15 09:39:06 -04:00
AK ClarkandClaude Opus 4.7 ebce0c073b Fix Tag_culture_test platform dependency on de-CH NumberGroupSeparator
The de-CH row hard-coded U+2019 as the expected thousands separator,
which only matches certain .NET/ICU/CLDR data versions. Linux .NET hosts
return U+0027 (ASCII apostrophe) for the same culture, so every Linux CI
run failed this test regardless of the actual change under test.

Resolve U+2019 in DataRow expectations to the runtime culture's
NumberGroupSeparator before comparison so the test stays stable across
hosts while still verifying the engine respects culture-specific
formatting.

- Added DataRows for de-DE (period) and ja-JP (comma)
- Added Samplerate_template_uses_culture_NumberGroupSeparator as an
  explicit regression guard that asserts the engine uses whatever the
  runtime CultureInfo reports

Fixes #1813.

Verified locally: 584 tests pass, 0 failed (565 succeeded, 19 skipped
Windows-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:10:57 -05:00
rmcrackan 0499bbae7e Merge pull request #1803 from Jo-Be-Co/1762_filter
1762 add filter() to list properties
2026-05-14 11:29:26 -04:00
Jo-Be-Co 4be0361e42 fixing typos and naming of test method 2026-05-14 16:51:51 +02:00
Jo-Be-Co c2c7b04acd minor fixes and comment corrections 2026-05-14 02:12:25 +02:00
Jo-Be-Co f194032139 #1762 add filter() to list properties 2026-05-14 02:10:38 +02:00
Jo-Be-Co a21bb8174d #1762 introduce CommonFormatters.TryGetLiteral 2026-05-14 02:04:26 +02:00
rmcrackan 3fc5ab0f8d Merge pull request #1807 from rmcrackan/rmcrackan/acct-export-disable
Account 'export' should be disabled when the account has no keys/tokens
2026-05-12 12:52:34 -04:00
rmcrackan 591a6d1594 Account 'export' should be disabled when the account has no keys/tokens 2026-05-12 12:05:11 -04:00
rmcrackan c4ac900045 Merge pull request #1806 from rmcrackan/rmcrackan/1804-win-folder-picker-custom-paths
#1804 - Fix Windows folder picker crash on custom paths
2026-05-12 09:04:18 -04:00
rmcrackan 50fde66a4c #1804 - Fix Windows folder picker crash on custom paths. Normalize stored paths before opening the OS folder dialog (\\?\ / UNC handling, existence check) in shared FolderPickerInitialPath, use it from WinForms and Avalonia folder pickers, and retry once if the dialog still throws. Adds small FileManager tests. 2026-05-12 08:55:12 -04:00
rmcrackan 597c8f7b3d Merge pull request #1795 from Jo-Be-Co/1762_unique_and_count
#1762 add unique() and count() to list properties
2026-05-09 23:21:29 -04:00
Jo-Be-Co 4ffcbeb1db Fix typos 2026-05-10 00:37:02 +02:00
Jo-Be-Co 6996fb1b81 When formatting ContributorDto, retain the IFormatProvider even if the format is empty 2026-05-10 00:36:49 +02:00
Jo-Be-Co 7ef10b1e7d #1762 add unique() and count() to list properties 2026-05-09 21:15:24 +02:00
Jo-Be-Co 0a644695d8 #1762 extract comparison to separate reusable class 2026-05-09 00:59:35 +02:00
rmcrackan 86b50b45ef Merge pull request #1796 from rmcrackan/rmcrackan/1711-linux-safety
#1711 : Linux/Docker: the default in-progress download/decrypt folder…
2026-05-08 09:10:23 -04:00
rmcrackan 727c869ba9 #1711 : Linux/Docker: the default in-progress download/decrypt folder is now per-user (/tmp/Libation-<username>) and is created with 0700 permissions, fixing failures caused by leftover /tmp/Libation directories from previous root installs and hardening against another local user reading partial download artifacts. Auto-upgrade downloads now land in a randomly-named per-run temp directory. 2026-05-08 09:04:39 -04:00
Robert f330d8bed6 incr ver 2026-05-05 13:45:27 -04:00
rmcrackan 9aecd26aea Merge pull request #1791 from rmcrackan/rmcrackan/1732-bug-fixes
#1732 bug fixes
2026-05-05 13:41:32 -04:00
rmcrackan 0cc2ef773d * Default Scan library to on for new accounts from Upsert / Mkb79 import (matches GUI)
* CLI liberate: print short license-denial reasons to stderr
* GUI: message when stoplight can’t queue (e.g. absent from last scan)
2026-05-05 13:32:28 -04:00
Robert 78371e3522 incr ver 2026-05-05 09:41:26 -04:00
rmcrackan 24a896e61b Merge pull request #1790 from Mbucari/master
Update Avalonia to v12
2026-05-05 09:39:52 -04:00
MBucari e4daa5b8e8 Fix macos bundle build failure 2026-05-04 21:20:42 -06:00
MBucari 505c614210 Code Cleanup
Remove unused parameters
Remove unnecessary casts
Make fields readonly
Order modifiers
Format document
Sort usings
Remove unnecessary nullable directive
Apply namespace preferences (file-level)
2026-05-04 21:10:28 -06: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 7b1dd64936 incr ver 2026-05-04 13:43:05 -04:00
rmcrackan c226e8ca38 Merge pull request #1789 from rmcrackan/rmcrackan/1732-feedback
Rmcrackan/1732 feedback
2026-05-04 13:42:01 -04:00
rmcrackan 673ff153b0 #1732 - new CLI list accounts option 2026-05-04 13:28:36 -04:00
rmcrackan 5b898dcd48 #1732 - fix: --help, -h, /?, /h, /help 2026-05-04 13:20:58 -04:00
rmcrackan b1df70f66c Merge pull request #1788 from rmcrackan/rmcrackan/1786-win-10
#1786 : win 10+ only
2026-05-04 11:06:02 -04:00
rmcrackan 220c916258 #1786 : win 10+ only 2026-05-04 11:05:32 -04:00
rmcrackan c3e0247940 Merge pull request #1787 from rmcrackan/rmcrackan/1783-chardonnay-x-filter
Bug fix #1783 : X to clear filter in Chardonnay
2026-05-04 10:12:01 -04:00
rmcrackan 0891fcf256 Bug fix #1783 : X to clear filter in Chardonnay 2026-05-04 10:02:18 -04:00
rmcrackan b259fdc9a5 Merge pull request #1785 from rmcrackan/rmcrackan/634-too-small
troubleshooting: if UI too small then AVALONIA_GLOBAL_SCALE_FACTOR
2026-05-04 09:35:56 -04:00
rmcrackan ecee08aaf7 troubleshooting: if UI too small then AVALONIA_GLOBAL_SCALE_FACTOR 2026-05-04 09:35:22 -04:00
rmcrackan b53a092327 Merge pull request #1784 from rmcrackan/rmcrackan/1199-docs
Add to linux troubleshooting, per solution in #1199
2026-05-04 09:07:21 -04:00
rmcrackan 0c838a483e Add to linux troubleshooting, per solution in #1199 2026-05-04 09:06:50 -04:00
rmcrackan d934511c32 incr ver 2026-05-01 10:11:51 -04:00
rmcrackan 8c88739644 Merge pull request #1747 from rmcrackan/rmcrackan/1732-cli-auth
add CLI auth options
2026-05-01 10:05:41 -04:00
rmcrackan 05e1de78da Merge pull request #1779 from rmcrackan/rmcrackan/1776-snap-docs
Make Linux Snap SQLite startup failures easier to understand
2026-05-01 10:01:45 -04: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 4f401fe6c7 Merge pull request #1778 from rmcrackan/rmcrackan/1744-imgs
trying again to fix the intermittent cover image issue
2026-05-01 09:46:35 -04:00
rmcrackan 849f5f508e trying again to fix the intermittent cover image issue 2026-05-01 09:38:29 -04:00
rmcrackan ac2b5ef3e2 Merge pull request #1777 from rmcrackan/rmcrackan/1776-improve-errors
#1776. better messages to users. improve all exception display
2026-04-30 11:11:26 -04:00
rmcrackan 13bdebbe9d #1776. better messages to users. improve all exception display 2026-04-30 11:04:50 -04:00
rmcrackan 6445a758a2 Merge pull request #1772 from rmcrackan/dependabot/npm_and_yarn/postcss-8.5.12
Bump postcss from 8.5.6 to 8.5.12
2026-04-27 19:08:01 -04:00
dependabot[bot] f6b0765dbc Bump postcss from 8.5.6 to 8.5.12
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.6 to 8.5.12.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.6...8.5.12)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 23:04:47 +00:00
rmcrackan 95dfee130e Merge pull request #1771 from Mbucari/master
Re-add Category Name Column to database
2026-04-27 19:03:47 -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
rmcrackan 0b975a168e Merge pull request #1769 from rmcrackan/rmcrackan/1744-folder-icons
#1744 Make Explorer folder icons reliable
2026-04-27 10:31:05 -04:00
rmcrackan f610ccdf58 #1744 Make Explorer folder icons reliable by running them after other post-download work, retrying briefly when the 300×300 cover or ICO write fails, and logging clear, actionable messages when it still cannot succeed. 2026-04-27 10:30:27 -04:00
rmcrackan f0c61ec7db typo 2026-04-23 11:20:32 -04:00
rmcrackan 14bd9d9deb Merge pull request #1759 from rmcrackan/rmcrackan/1694-mac-docs
Improve mac troubleshooting docs per discoveries in #1694
2026-04-23 11:13:42 -04:00
rmcrackan 01487690d4 Improve mac troubleshooting docs per discoveries in #1694 2026-04-23 11:13:17 -04:00
rmcrackan acfde519f5 Merge pull request #1758 from rmcrackan/rmcrackan/fix-naming-template
fix naming template markdown
2026-04-22 13:50:14 -04:00
rmcrackan 7fb6ea4701 fix {{ literal 2026-04-22 13:47:29 -04:00
rmcrackan db1566a32d This font is too clever with rendering things like <- as arrows. Let's just make this page monospace 2026-04-22 13:47:01 -04:00
rmcrackan ca48767cc1 syntax fixes 2026-04-22 10:45:57 -04:00
rmcrackan 310abe951a Merge pull request #1757 from rmcrackan/dependabot/github_actions/apple-actions/import-codesign-certs-7
Bump apple-actions/import-codesign-certs from 6 to 7
2026-04-21 10:24:34 -04:00
dependabot[bot] aed2aec38e Bump apple-actions/import-codesign-certs from 6 to 7
Bumps [apple-actions/import-codesign-certs](https://github.com/apple-actions/import-codesign-certs) from 6 to 7.
- [Release notes](https://github.com/apple-actions/import-codesign-certs/releases)
- [Commits](https://github.com/apple-actions/import-codesign-certs/compare/v6...v7)

---
updated-dependencies:
- dependency-name: apple-actions/import-codesign-certs
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 14:14:34 +00:00
rmcrackan 531d98a0d5 Merge pull request #1756 from rmcrackan/rmcrackan/fix-docs
fix vitepress/vue syntax
2026-04-21 08:56:00 -04:00
rmcrackan dd8bba959d fix vitepress/vue syntax 2026-04-21 08:54:53 -04:00
Robert 9d6f0f7700 incr ver 2026-04-20 22:01:31 -04:00
rmcrackan 274cbf37ca Merge pull request #1755 from Jo-Be-Co/Conditional-output-on-format-templates
#1754 Conditional output on format templates
2026-04-20 21:59:02 -04:00
Jo-Be-Co 2b25ced65b documentation for format-templates 2026-04-21 00:18:14 +02:00
Jo-Be-Co 417e654799 #1754 Conditional output on format templates 2026-04-20 23:15:54 +02:00
rmcrackan e9550055c6 Merge pull request #1750 from Jo-Be-Co/refactor-format-escapes
fixed quoting errors in format-strings
2026-04-20 16:46:43 -04:00
Jo-Be-Co fa64be7ad0 fixed typoo ;-) 2026-04-20 22:41:34 +02:00
Jo-Be-Co e3508b2998 rebased and small fixes 2026-04-20 22:06:40 +02:00
Jo-Be-Co 30809fb38d fixed quoting errors in format-strings 2026-04-20 21:38:45 +02:00
rmcrackan 5b89f29a4f Merge pull request #1735 from Jo-Be-Co/bifunctions
1714 conditional tag cmp working on two properties
2026-04-20 14:36:24 -04:00
Jo-Be-Co ea33d8933a test added with colons in tag comparison 2026-04-20 19:24:21 +02:00
Jo-Be-Co c1e9911ade Small fixes on auto checks 2026-04-20 18:43:33 +02:00
Jo-Be-Co 962a5055b2 Fixed PR comments 2026-04-20 18:07:17 +02:00
rmcrackan 3108bfa469 Merge pull request #1753 from rmcrackan/rmcrackan/1748-upgrader
In-app upgrades now wait for the real installer and treat failures as failures
2026-04-20 10:21:13 -04:00
rmcrackan ccc74cef56 #1748: In-app upgrades now wait for the real installer and treat failures as failures. 2026-04-20 10:20:29 -04:00
rmcrackan 1de778adef Merge pull request #1752 from rmcrackan/rmcrackan/1751-resizeable
#1751 - text should always be readable
2026-04-20 09:48:38 -04:00
rmcrackan 4a30acf0eb #1751 - text should always be readable. Allow for user-resizing when it's not the case. 2026-04-20 09:48:13 -04:00
rmcrackan 6d326ebabc #1732 add CLI auth options. Add to the CLI these features which are already available in the GUI: importing from @mkb79 's audible-cli , and the external browser login (aka: alternative login). Caveat: 2nd feature doesn't work for Brazil (gui or cli) 2026-04-18 15:29:57 -04:00
rmcrackan f2ef616203 Merge pull request #1745 from Jo-Be-Co/refactor-LocaleDto
Some optimizations after last update to AudibleApi
2026-04-17 17:53:07 -04:00
Jo-Be-Co 87e8c6a32a fixed pr remarks 2026-04-17 23:48:22 +02:00
rmcrackan 62dfc20255 Merge pull request #1746 from rmcrackan/rmcrackan/1744-images
#1744 - attempt to harded code for cover image/folder image
2026-04-17 16:11:21 -04:00
rmcrackan 9f255a3bc1 #1744 - attempt to harded code for cover image/folder image 2026-04-17 16:07:00 -04:00
Jo-Be-Co 6d7af0a8ae Some optimizations after last update to AudibleApi 2026-04-17 21:43:42 +02:00
Jo-Be-Co 650b9c7f5b added docs for cmp tag 2026-04-17 21:22:30 +02:00
Jo-Be-Co dc353aa7e5 Merge branch 'master' into bifunctions 2026-04-17 08:50:53 +02:00
Jo-Be-Co d5258b7e64 fixed merge collision 2026-04-17 08:46:52 +02:00
Jo-Be-Co 475f20b3cb unit tests added for new list operations 2026-04-17 08:35:43 +02:00
rmcrackan 6efc64b927 incr ver 2026-04-16 12:27:11 -04:00
rmcrackan d2cc9ff26f Merge pull request #1743 from rmcrackan/rmcrackan/corrupt-search
Attempt to recover from corrupt search engine
2026-04-15 13:32:14 -04:00
rmcrackan cfe4770387 Attempt to recover from corrupt search engine 2026-04-15 13:24:26 -04:00
rmcrackan 33967adb15 Merge pull request #1742 from rmcrackan/rmcrackan/harden-webview
harden webview for linux
2026-04-15 10:22:39 -04:00
rmcrackan 9ef8dca4d5 Document Linux runtime dependency 2026-04-15 09:58:36 -04:00
rmcrackan af1029f4b5 Honor "don't use WebView" for this path 2026-04-15 09:50:18 -04:00
rmcrackan 63e7f0f44e Automatic fallback when WebKit is missing. If embedded WebView cannot load, route the same “add account” flow to non-WebView login, so Arch minimal installs work without WebKit. 2026-04-15 09:45:35 -04:00
rmcrackan 65623e8e81 Catch WebKit load failures on account login. Wrap the code path that opens NativeWebDialog / WebView for Audible login in a try/catch for DllNotFoundException (and related load failures). On failure, show a short dialog: install WebKit2GTK and do not crash the app. 2026-04-15 09:41:47 -04:00
rmcrackan a5912efa0c upgrade nuget 2026-04-15 08:10:09 -04:00
rmcrackan dba3e6ca8e Merge pull request #1725 from Jo-Be-Co/language_and_region
Extend <language> and <locale> tags
2026-04-15 08:00:49 -04:00
Jo-Be-Co a4bc39ee6b switched from RegionInfo to AudibleApi.Locale 2026-04-15 00:45:55 +02:00
rmcrackan 675fecec67 Merge pull request #1737 from rmcrackan/dependabot/github_actions/actions/upload-pages-artifact-5
Bump actions/upload-pages-artifact from 4 to 5
2026-04-14 10:22:55 -04:00
dependabot[bot] be1eac5692 Bump actions/upload-pages-artifact from 4 to 5
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 14:14:02 +00:00
Jo-Be-Co 8bad7d90c3 List os and ui tag in documentation 2026-04-14 03:04:26 +02:00
Jo-Be-Co c72e5a3568 fixed matching of first property 2026-04-14 02:48:03 +02:00
rmcrackan e145e46579 Merge pull request #1731 from Jo-Be-Co/1729_docker_resilient-creation-of-db-link
1729 docker resilient creation of db link
2026-04-13 19:38:44 -04:00
Jo-Be-Co 5fd8ac481b review fixes 2026-04-14 01:14:45 +02: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 3456eb6c35 Merge remote-tracking branch 'upstream/master' into 1729_docker_resilient-creation-of-db-link
# Conflicts:
#	Docker/liberate.sh
2026-04-14 00:51:09 +02:00
Jo-Be-Co c1b8cc894a add mathematical symbols 2026-04-14 00:41:22 +02:00
Jo-Be-Co 56c15d3108 add checks on lists 2026-04-14 00:38:41 +02:00
Jo-Be-Co 810ea90770 introduce cmp tag as check with two operands 2026-04-14 00:30:34 +02:00
rmcrackan b90cc67b09 Merge pull request #1734 from rmcrackan/dependabot/github_actions/softprops/action-gh-release-3
Bump softprops/action-gh-release from 2 to 3
2026-04-13 11:34:55 -04:00
dependabot[bot] 550a99ac90 Bump softprops/action-gh-release from 2 to 3
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 15:30:12 +00:00
rmcrackan 9ed89eca9e Merge pull request #1733 from rmcrackan/rmcrackan/1729-docker-tweak
#1729 minor docker tweak
2026-04-13 08:51:28 -04:00
rmcrackan 197d645e10 #1729 minor docker tweak 2026-04-13 08:51:07 -04:00
rmcrackan bdfc99f209 Merge pull request #1730 from rmcrackan/dependabot/github_actions/actions/github-script-9
Bump actions/github-script from 8 to 9
2026-04-13 08:37:44 -04:00
Jo-Be-Co a010da5251 #1714 Convert the evaluation of the 'is' tag into two-parameter logic 2026-04-13 01:39:16 +02:00
Jo-Be-Co 4c212e752f some refinements 2026-04-11 22:19:31 +02:00
Jo-Be-Co e8d2ac5c62 #1729 added resilience when creating the link to the database file 2026-04-11 21:22:54 +02:00
Jo-Be-Co 6c552623d9 Adding documentation for region and language tags 2026-04-11 01:59:49 +02:00
dependabot[bot] 5a0d62687c Bump actions/github-script from 8 to 9
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 14:13:05 +00:00
Jo-Be-Co f4e61d0445 hardend an commented unit tests for region output 2026-04-10 11:44:28 +02:00
Jo-Be-Co 54765fc181 Fixed cross-platform problems on region tests 2026-04-10 10:14:19 +02:00
Jo-Be-Co d292cb0472 Merge remote-tracking branch 'origin/language_and_region' into language_and_region
# Conflicts:
#	Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs
2026-04-10 09:47:00 +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 e7d08d30b0 Merge pull request #1727 from rmcrackan/rmcrackan/log-init-db
Log initial db state, esp to capture books in trash
2026-04-09 11:05:36 -04:00
rmcrackan 26946e8a87 Log initial db state, esp to capture books in trash 2026-04-09 10:57:41 -04:00
rmcrackan ecf1cf4404 Merge pull request #1726 from rmcrackan/rmcrackan/vulnerable-nuget
copilot fix to vulnerable nuget pkg
2026-04-09 10:03:54 -04:00
rmcrackan 8213521404 copilot fix to vulnerable nuget pkg 2026-04-09 09:57:41 -04:00
Jo-Be-Co 0ef73464b8 Extend <language> and <locale> tags to allow different outputs like ISO-codes or names in different languages 2026-04-09 02:33:05 +02:00
rmcrackan 3030b748d7 Merge pull request #1723 from rmcrackan/rmcrackan/sqlite-readonly
Better handling of sqlite readonly
2026-04-08 17:48:55 -04:00
rmcrackan ccef50d681 move new check into datalayer 2026-04-08 17:43:21 -04:00
rmcrackan 78e6d7c5c1 Better handling of sqlite readonly 2026-04-08 17:33:07 -04:00
rmcrackan 846b93c97c Merge pull request #1720 from rmcrackan/rmcrackan/1719-corrupt-search-engine
#1719 - add defensive code to recover from a corrupt search engine
2026-04-07 15:05:45 -04:00
rmcrackan b0e79faacf #1719 - add defensive code to recover from a corrupt search engine 2026-04-07 15:00:18 -04:00
rmcrackan 3fece6aa44 Merge pull request #1718 from rmcrackan/rmcrackan/1716
#1716 - fix NAS bug
2026-04-05 15:47:41 -04:00
rmcrackan a7c7bd9925 #1716 - fix NAS bug 2026-04-05 15:41:58 -04:00
rmcrackan 3373b0979a Merge pull request #1713 from Jo-Be-Co/first-tag
Add `<first tag[...]>`
2026-04-03 09:19:50 -04:00
Jo-Be-Co 3e3da2298e Add <first tag[...]> 2026-04-03 03:48:09 +02:00
rmcrackan a9cb560f95 Merge pull request #1709 from rmcrackan/rmcrackan/plus-warning
Give better feedback to user with 'plus' license denied
2026-03-31 17:42:51 -04:00
rmcrackan e4282f014e remove close issues search link 2026-03-31 17:38:32 -04:00
rmcrackan b694e319f7 Give better feedback to user when they're getting persistent 'license denied' errors which are likely caused by too many Plus downloads 2026-03-31 17:31:40 -04:00
rmcrackan 67e3e41b15 Merge pull request #1708 from rmcrackan/rmcrackan/1704-mkb79import
fix bug with importing mkb79 auth with website_cookies: null vs empty
2026-03-31 13:22:33 -04:00
rmcrackan ad2101ab8c fix bug with importing mkb79 auth with website_cookies: null vs empty 2026-03-30 15:10:55 -04:00
rmcrackan d7ea8eb8f1 Merge pull request #1707 from rmcrackan/rmcrackan/webview-err-msg
better error messages when webview is the problem
2026-03-30 14:22:51 -04:00
rmcrackan 5c1153e187 better error messages when webview is the problem. shouldn't appear like a scan error 2026-03-30 14:22:09 -04:00
rmcrackan e929ab3062 Merge pull request #1706 from rmcrackan/dependabot/github_actions/actions/configure-pages-6
Bump actions/configure-pages from 5 to 6
2026-03-30 11:43:22 -04:00
dependabot[bot] d64fd564ab Bump actions/configure-pages from 5 to 6
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 5 to 6.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 15:38:21 +00:00
rmcrackan 340b0a0d29 Merge pull request #1705 from rmcrackan/rmcrackan/fix-template-esc
fix escaped html tags
2026-03-30 11:27:51 -04:00
rmcrackan 1f217b3966 fix escaped html tags 2026-03-30 11:26:53 -04:00
rmcrackan accb43456b Merge pull request #1700 from rmcrackan/rmcrackan/fix-templates-docs
fix tags in templates doc
2026-03-27 16:22:57 -04:00
rmcrackan b3a781de5e fix tags in templates doc 2026-03-27 16:21:32 -04:00
rmcrackan 34613f7e97 Merge pull request #1699 from rmcrackan/rmcrackan-patch-1
Update naming-templates.md
2026-03-27 16:02:38 -04:00
rmcrackan 1def16a157 Update naming-templates.md 2026-03-27 16:02:27 -04:00
rmcrackan 33a209df3c Merge pull request #1690 from Jo-Be-Co/improve-formatting-tags
Improve formatting tags
2026-03-27 15:56:51 -04:00
Jo-Be-Co 2daf2da30a Test fixes 2026-03-27 09:11:00 +01:00
Jo-Be-Co dd2bc7c461 Documentation for TimeSpan. 2026-03-27 01:14:42 +01:00
Jo-Be-Co 1d1936734b Test hardening.
Ensure tests check error and warning output to confirm assertions are correctly formulated.
2026-03-27 00:06:17 +01:00
rmcrackan e7f6e71d3b Merge pull request #1698 from rmcrackan/1692-mac-rt-clk-attempt2
#1692 : 2nd attempt to fix mac rt-clk issue
2026-03-26 16:23:51 -04:00
rmcrackan 0da8af3b8f incr ver 2026-03-26 16:17:51 -04:00
rmcrackan f159bcdbb9 2nd attempt 2026-03-26 15:26:06 -04:00
rmcrackan fc85aa457c Merge pull request #1695 from rmcrackan/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-03-26 10:40:55 -04:00
rmcrackan 687a51b7bc Merge pull request #1696 from rmcrackan/dependabot/github_actions/actions/deploy-pages-5
Bump actions/deploy-pages from 4 to 5
2026-03-26 10:40:25 -04:00
dependabot[bot] 477d0088e2 Bump actions/deploy-pages from 4 to 5
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-26 14:12:37 +00:00
dependabot[bot] 38138d8272 Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-26 14:12:33 +00:00
Jo-Be-Co fc92c633e1 No made up format for minutes as .NET already knows how to format them.
User documentation needs to follow.
2026-03-26 03:12:24 +01:00
Jo-Be-Co 3c71cb10b2 some minor fixes 2026-03-26 03:10:43 +01:00
rmcrackan 65d8adc018 Merge pull request #1693 from rmcrackan/rmcrackan/close-stale
#1532 -- issue cleanup. Close stale tickets. Thanks @HenraL !
2026-03-25 11:13:30 -04:00
rmcrackan 9e739e48d7 #1532 -- issue cleanup. Close stale tickets. Thanks @HenraL ! 2026-03-25 10:59:12 -04:00
Jo-Be-Co 6b26280aa8 Get formats for properties in given form to the formatters by no unescaping them in advance. 2026-03-25 14:23:03 +01:00
Jo-Be-Co 14d8bb2205 As stated in the "Naming Templates" documentation, support custom formatters on numbers.
Only trim spaces on tags where they are disruptive.
2026-03-25 02:34:32 +01:00
rmcrackan 0960edeafb Add contributor: Jo-Be-Co 2026-03-24 15:16:17 -04:00
Jo-Be-Co 6d2b0b952d - Inspect exception on regexp timeout
- special tests for checks with whitespace
- Escaping allows whitespace at the edges
2026-03-24 00:18:40 +01:00
Jo-Be-Co fb277cff81 Faulty regex patterns will throw InvalidOperationException during evaluation. 2026-03-23 15:29:32 +01:00
Jo-Be-Co dbd5af8ab0 QS second part:
Made Evaluate() more resilient.
Added tests to CommonFormatters.
2026-03-22 22:22:02 +01:00
Jo-Be-Co 02b9b4fa1c QS updates:
- fixed documentation
- regexp-checks running with timeout and culture-invariant matching
- changed check-building in ConditionalTagCollection to use NonNull parameters. So no warnings occure.
- add tests for <!is ...> and escaped chars
2026-03-22 19:18:55 +01:00
Jo-Be-Co 7ecd571364 Corrections to documentation and comments. 2026-03-22 13:55:28 +01:00
Jo-Be-Co 32f3e6a886 Introduces the slice() format directive 2026-03-22 13:55:27 +01:00
Jo-Be-Co e477c29890 New tags <minutes>, <tag> and <if abridged->...<-if abridged> 2026-03-22 13:55:27 +01:00
Jo-Be-Co a8621699c1 Add numerical checks for conditional tag.
- numbers are compared with their value
- strings are compared by checking the length
- Collections are evaluated on cheking their size
2026-03-22 13:55:27 +01:00
Jo-Be-Co cd9a070784 Allow two step formatting to get checkable values and not only single stringa
Introduce <is-> Tag. Like <has-> but with additional check on content.

Retrieve objects instead of string for conditions

Pass undefined formats as null instead of empty strings
2026-03-22 13:55:26 +01:00
Jo-Be-Co d8ce7cc9b0 Formats are now escapable and the evaluation of HasValue has been transformed into a configuration parameter. 2026-03-22 13:55:26 +01:00
Jo-Be-Co 1d6d4ff9ac GetTagExpression uses Dictionary for capture names from Regexes 2026-03-22 13:55:26 +01:00
Jo-Be-Co db3a810f47 Regular Expressions with capture names on coments 2026-03-22 13:55:26 +01:00
Jo-Be-Co 508ea1032d Tests are using empty values for various fields 2026-03-22 13:55:25 +01:00
Jo-Be-Co 10fc5837cb Allow sorting in reverse order by specifying the sort key with lowercase 2026-03-22 13:55:25 +01:00
Jo-Be-Co 812e0c3b60 Format details on fields specified in format. Introduce by colon.
Use dictionaries for field access and sorting.

Added sorting to series lists.
2026-03-22 13:55:25 +01:00
Jo-Be-Co d161bdfaeb Introduce culture parameter for formatting. 2026-03-22 13:55:24 +01:00
Jo-Be-Co a6aa14c7f8 Collect formatters into CommonFormatters 2026-03-22 13:55:24 +01:00
Jo-Be-Co 7dd55a2545 Refactorings 2026-03-22 13:55:24 +01:00
rmcrackan 1081dfe8df Merge pull request #1689 from rmcrackan/rmcrackan/mac-yaml
mac yaml. print stdout, stderr
2026-03-18 14:53:27 -04:00
rmcrackan 3f716cd1ac - **Fix:** When xcrun notarytool submit failed (e.g. Apple TOS / "sign agreement" required), the job failed immediately and the real error never appeared in the workflow log.
- **Change:** Capture notarytool stdout+stderr with `|| true` so the script always runs `echo "$RESPONSE"`, then fail the step explicitly if no submission id is found. The job still fails on errors, but the full notarytool output (including stderr) is now visible in the Actions log.
- **Result:** Failures like "you must accept the agreement" show up in the run so you don’t have to reproduce them locally to see the message.
2026-03-18 14:49:02 -04:00
Robert 338715d52a incr ver 2026-03-17 23:07:30 -04:00
rmcrackan d275ad034b Merge pull request #1688 from rmcrackan/rmcrackan/macos-build
attempt to fix Bundle_MacOS
2026-03-17 23:04:23 -04:00
rmcrackan b5ecff7e27 attempt to fix Bundle_MacOS 2026-03-17 23:03:43 -04:00
Robert 3144506ad5 incr ver 2026-03-17 22:30:05 -04:00
rmcrackan 99629c3c25 Merge pull request #1678 from rmcrackan/rmcrackan/screenreader-msg
Add info in walkthrough for screen-reader users
2026-03-17 22:19:07 -04:00
rmcrackan 3fdb8ef15d Merge pull request #1679 from rmcrackan/rmcrackan/1677-linux-crash
Rmcrackan/1677 linux crash
2026-03-17 22:18:54 -04:00
rmcrackan a43a700e37 Merge pull request #1686 from rmcrackan/rmcrackan/empty-cookies
don't set cookies without values. Attempting to solve #1682
2026-03-17 22:18:45 -04:00
rmcrackan 80823bf15c don't set cookies without values. Attempting to solve #1682 2026-03-17 16:59:11 -04:00
rmcrackan 0a576069df clean up essential file validation 2026-03-12 15:27:04 -04:00
rmcrackan 0b0f5184d2 Validate essential files early 2026-03-12 14:55:14 -04:00
rmcrackan 24d825607d * Reads relative paths from appsettings and resolves them to absolute once.
* Writes only absolute paths for LibationFiles, so future reads and all callers see a single, consistent path.
2026-03-12 13:57:39 -04:00
rmcrackan 14e4ea4592 Resolve to an absolute path when reading 2026-03-12 13:53:27 -04:00
rmcrackan 56720124b8 Add info in walkthrough for screen-reader users 2026-03-12 11:43:06 -04:00
Robert 20c7d72181 incr ver 2026-03-12 09:30:22 -04:00
rmcrackan 0d00cf2b10 Merge pull request #1676 from rmcrackan/rmcrackan/1625-mac-key-bindings
Bug fix #1625 -- fix mac key bindings
2026-03-12 09:23:16 -04:00
rmcrackan a79da66b0e Bug fix #1625 -- fix mac key bindings 2026-03-12 09:16:41 -04:00
rmcrackan fdc56e9ad6 Merge pull request #1673 from rmcrackan/rmcrackan/fix-naming-template-links
Fix naming template links
2026-03-11 13:54:23 -04:00
rmcrackan ddef3b649b Fix naming template links 2026-03-11 13:45:59 -04:00
rmcrackan 01c2a3b1c3 Merge pull request #1672 from rmcrackan/rmcrackan/linux-snap-no-webview
WebView breaks catastrophically under Linux Snap
2026-03-11 13:27:34 -04:00
rmcrackan 4eec61a659 another build fix attempt 2026-03-11 13:23:21 -04:00
rmcrackan c983513f45 attempt to fix build failures 2026-03-11 13:20:44 -04:00
rmcrackan 0172c7b8eb add 'linux' to wording 2026-03-11 13:07:10 -04:00
rmcrackan 5cf710756f Bug fix #1664 - WebView breaks catastrophically under Linux Snap -- segfault with no logged errors 2026-03-11 13:00:19 -04:00
rmcrackan 760b0dcb6b Merge pull request #1670 from rmcrackan/rmcrackan/access1668-1
First first 2 items in #1668 -- accessibility bugs
2026-03-10 15:53:44 -04:00
rmcrackan 6c602286c0 First first 2 items in #1668 -- accessibility bugs 2026-03-10 15:51:25 -04:00
rmcrackan 8e69191bbf Merge branch 'master' of https://github.com/rmcrackan/Libation 2026-03-07 16:21:11 -05:00
rmcrackan 58095f291b When displaying upgrade notes, exclude <!-- NO-APP --> blocks 2026-03-07 16:21:04 -05:00
rmcrackan 50319defa9 Merge pull request #1666 from rmcrackan/rmcrackan-patch-2
Update command-line-interface.md
2026-03-07 15:29:26 -05:00
rmcrackan fd35b6dd85 Update command-line-interface.md 2026-03-07 15:29:10 -05:00
rmcrackan 2da2d1c29e Merge pull request #1665 from rmcrackan/rmcrackan-cli-details-block
Update command-line-interface.md
2026-03-07 15:27:26 -05:00
rmcrackan 64c5807daa Update command-line-interface.md 2026-03-07 15:27:09 -05:00
rmcrackan 1e8358eeeb Merge pull request #1663 from cvigano/add-replacement-char-documentation
Add documentation on how to customize replacement characters on CLI
2026-03-07 15:22:34 -05:00
Christoph Vigano 2868039772 Add documentation on how to customize replacement characters on CLI 2026-03-07 13:55:34 +01:00
rmcrackan 45af5aa913 Merge pull request #1662 from rmcrackan/dependabot/github_actions/docker/build-push-action-7
Bump docker/build-push-action from 6 to 7
2026-03-06 09:54:49 -05:00
rmcrackan 487fcb007c Merge pull request #1661 from rmcrackan/dependabot/github_actions/docker/metadata-action-6
Bump docker/metadata-action from 5 to 6
2026-03-06 09:54:19 -05:00
dependabot[bot] 76d4a44040 Bump docker/build-push-action from 6 to 7
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:13:14 +00:00
dependabot[bot] 07d9fff2b3 Bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:13:10 +00:00
rmcrackan bf1e90c33c incr ver 2026-03-05 12:11:03 -05:00
rmcrackan c30a79d97b Fix release workflow YAML: quote body to avoid < parse error 2026-03-05 12:08:03 -05:00
Robert 737121963f incr ver 2026-03-05 11:54:07 -05:00
rmcrackan 6b319fe1a7 Merge pull request #1658 from rmcrackan/rmcrackan/bugfix-save-templates
Bug fix #1657 - error in saving templates
2026-03-05 11:47:23 -05:00
rmcrackan 70ef1c52a4 Bug fix #1657 - error in saving templates 2026-03-05 11:46:35 -05:00
rmcrackan bdf5f2faea Merge pull request #1656 from rmcrackan/rmcrackan/widget-edits
#1652 - widget eidts
2026-03-05 10:16:57 -05:00
rmcrackan 5ff3dc7581 #1652 - widget eidts 2026-03-05 10:16:25 -05:00
rmcrackan 41fcef207b Merge pull request #1655 from rmcrackan/rmcrackan/widget-styling
#1652 - edit widget styling
2026-03-05 10:08:02 -05:00
rmcrackan 4edc77aa00 #1652 - edit widget styling 2026-03-05 10:07:31 -05:00
rmcrackan 6d05391151 Merge pull request #1654 from rmcrackan/rmcrackan/vitepress-fix
#1652: Fix vitepress publish
2026-03-05 09:49:16 -05:00
rmcrackan f9e2af6916 Fix vitepress publish 2026-03-05 09:48:57 -05:00
rmcrackan 75e9a7db1f Merge pull request #1653 from rmcrackan/rmcrackan/dl-suggester
#1652: add a homepage widget: download version suggester
2026-03-05 09:41:02 -05:00
rmcrackan ee92988125 #1652: add a homepage widget: download version suggester 2026-03-05 09:35:53 -05:00
rmcrackan c005af2457 Merge pull request #1651 from rmcrackan/dependabot/github_actions/docker/setup-buildx-action-4
Bump docker/setup-buildx-action from 3 to 4
2026-03-05 09:25:24 -05:00
dependabot[bot] 89ae5709a9 Bump docker/setup-buildx-action from 3 to 4
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-05 14:13:42 +00:00
rmcrackan 9c9f1c67c8 Merge pull request #1650 from rmcrackan/rmcrackan/autoscan-default-false
AutoScan defaultValue: false
2026-03-04 15:12:55 -05:00
rmcrackan d637068f64 AutoScan defaultValue: false 2026-03-04 15:12:25 -05:00
rmcrackan 490d292c18 Merge pull request #1648 from rmcrackan/dependabot/github_actions/docker/setup-qemu-action-4
Bump docker/setup-qemu-action from 3 to 4
2026-03-04 09:39:09 -05:00
rmcrackan 8417b121a4 Merge pull request #1647 from rmcrackan/dependabot/github_actions/docker/login-action-4
Bump docker/login-action from 3 to 4
2026-03-04 09:38:45 -05:00
dependabot[bot] 72edd1c896 Bump docker/setup-qemu-action from 3 to 4
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 14:14:26 +00:00
dependabot[bot] 4e8306470c Bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 14:14:23 +00:00
rmcrackan bdbeeb8935 Merge pull request #1646 from rmcrackan/rmcrackan/cli-doc-progressbar
document CLI progress bar option
2026-03-03 17:19:47 -05:00
rmcrackan 1b12dd04ff document CLI progress bar option 2026-03-03 17:18:38 -05:00
rmcrackan 589b806aff Merge pull request #1645 from rmcrackan/rmcrackan/cli-docs
Break out CLI into its own page
2026-03-03 16:45:28 -05:00
rmcrackan c248bc0a04 Break out CLI into its own page 2026-03-03 16:44:56 -05:00
Robert db4ef4b321 incr ver 2026-03-03 15:44:12 -05:00
rmcrackan ca7d3feaf9 Merge pull request #1642 from rmcrackan/rmcrackan/improved-upgrade-check
Upgrade checks to include: yes, no, don't know
2026-03-02 08:47:55 -05:00
rmcrackan 2b0b1ac798 comment 2026-03-02 08:47:28 -05:00
rmcrackan 41e0aa0dfb Upgrade checks to include: yes, no, don't know 2026-03-02 08:44:18 -05:00
rmcrackan f3eb4d9f87 Merge pull request #1641 from rmcrackan/rmcrackan-linux-deb-version
Update .releaseindex.json. Add Linux DEB
2026-03-02 08:41:14 -05:00
rmcrackan ae157a1506 Update .releaseindex.json 2026-03-02 08:40:55 -05:00
rmcrackan a7654745b6 Merge pull request #1640 from rmcrackan/rmcrackan/lucene-exp-backoff
Lucene search engine retries to use exponential backoff
2026-03-02 08:05:21 -05:00
rmcrackan 8dad104121 fix file encoding 2026-03-02 08:04:48 -05:00
rmcrackan 5bf1438e00 exp bak comments 2026-03-02 08:04:04 -05:00
rmcrackan da3d057372 Lucene search engine retries to use exponential backoff 2026-03-02 07:59:45 -05:00
Robert 9fec47e8ab incr ver 2026-03-01 10:07:44 -05:00
rmcrackan a218ed5e66 update AudibleApi 2026-02-28 17:03:51 -05:00
rmcrackan 4f28268785 Merge pull request #1638 from rmcrackan/rmcrackan/lucene-corrupt
Bug fix #1628 , #1636 : Address lucene search engine corruption
2026-02-28 16:29:06 -05:00
rmcrackan e3e55a30e1 Bug fix #1628 , #1636 : Address lucene search engine corruption 2026-02-28 16:28:33 -05:00
rmcrackan b157d760ec Merge pull request #1634 from rmcrackan/dependabot/npm_and_yarn/rollup-4.59.0
Bump rollup from 4.53.4 to 4.59.0
2026-02-27 09:55:55 -05:00
dependabot[bot] 60c532a3bc Bump rollup from 4.53.4 to 4.59.0
Bumps [rollup](https://github.com/rollup/rollup) from 4.53.4 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.53.4...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 14:54:46 +00:00
rmcrackan 2a348df4ac Merge pull request #1633 from rmcrackan/dependabot/github_actions/actions/upload-artifact-7
Bump actions/upload-artifact from 6 to 7
2026-02-27 09:54:24 -05:00
rmcrackan bd881d4dc4 Merge pull request #1632 from rmcrackan/dependabot/github_actions/actions/download-artifact-8
Bump actions/download-artifact from 7 to 8
2026-02-27 09:53:47 -05:00
dependabot[bot] a6a9b33d01 Bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 14:13:38 +00:00
dependabot[bot] a77fe73153 Bump actions/download-artifact from 7 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 14:13:29 +00:00
rmcrackan 8d77787df7 Merge pull request #1622 from rmcrackan/rmcrackan-lucene-attrib
Update lucene.md
2026-02-22 09:03:30 -05:00
rmcrackan 44fe5b95f1 Update lucene.md 2026-02-22 09:03:03 -05:00
rmcrackan b8d7a956cd Merge pull request #1615 from rmcrackan/rmcrackan-lucene-4
Update searching-and-filtering.md
2026-02-18 09:06:27 -05:00
rmcrackan 002bdde444 Update searching-and-filtering.md
Attempting to force update for docs sidebar
2026-02-18 09:06:14 -05:00
rmcrackan 642eeecc0a Merge pull request #1614 from rmcrackan/rmcrackan-lucene-docs-3
Update config.js
2026-02-18 09:02:53 -05:00
rmcrackan 3343ef263c Update config.js 2026-02-18 09:02:33 -05:00
rmcrackan 6cf51c63c6 Merge pull request #1613 from rmcrackan/rmcrackan-patch-1
Update index.md
2026-02-18 08:58:09 -05:00
rmcrackan c34342ecb5 Update index.md 2026-02-18 08:57:56 -05:00
rmcrackan 53e5b635a3 Merge pull request #1612 from rmcrackan/rmcrackan-lucene-docs
Update searching-and-filtering.md
2026-02-18 08:54:41 -05:00
rmcrackan e6b12914a4 Update searching-and-filtering.md 2026-02-18 08:54:09 -05:00
rmcrackan 28327efd7b Create lucene.md 2026-02-18 08:52:38 -05:00
rmcrackan f17af04908 Merge pull request #1601 from Mbucari/master
Enable project-wide nullable reference types
2026-02-09 17:30:06 -05: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
rmcrackan f15e073204 Merge pull request #1589 from rmcrackan/rmcrackan-master-1
Update config.js
2026-02-03 10:07:39 -05:00
rmcrackan f00951d615 Update config.js
"Edit this page" went to 'main' instead of 'master'
2026-02-03 10:07:17 -05:00
Robert c7e844a54c incr ver 2026-02-03 09:38:30 -05:00
rmcrackan 178cc82d65 Merge pull request #1587 from Mbucari/master
Documentation and bug fixes
2026-02-03 09:05:06 -05:00
Michael Bucari-Tovo 054f7437d1 Fix importer NRE 2026-02-02 16:02:44 -07:00
Mbucari 99051b6975 Merge branch 'rmcrackan:master' into master 2026-01-31 23:52:05 -07:00
Robert eed7f1811b incr ver 2026-01-31 23:33:29 -05:00
rmcrackan 33cd6b8639 Merge pull request #1585 from thoughtbox/patch-2
Fix typos
2026-01-31 23:31:16 -05:00
rmcrackan 06014f467e Merge pull request #1581 from scagood/patch-1
Remove progress bar in non interactive mode (#1481)
2026-01-31 23:29:04 -05:00
Tor Houghton b2eef18217 Fix typos
buit-in -> built-in
broswer -> browser
2026-01-31 22:19:53 +01:00
Robert 443d1f64ca incr ver 2026-01-31 12:36:46 -05:00
Robert c43e88d269 verbose null checking to debug #1578 2026-01-31 12:36:29 -05:00
Michael Bucari-Tovo dc1919f411 Update AAXClean to 3.0.2 and fix deprecation warnings 2026-01-30 14:46:11 -07:00
Mbucari fdfae2f806 Merge branch 'rmcrackan:master' into master 2026-01-30 14:42:13 -07:00
Sebastian Good 5c56e1d39b Correct small spelling mistake 2026-01-30 16:12:48 +00:00
Sebastian Good b39e2c3e0b Remove progress bar in non interactive mode (#1481) 2026-01-30 16:12:44 +00:00
Robert 946d4779a9 incr ver 2026-01-28 07:47:10 -05:00
Robert e620d0be24 remove unneeded coalesce 2026-01-28 07:44:32 -05:00
Mbucari d58f5abe35 Add Troubleshooting link to Advanced topics
Added 'Troubleshooting' link to the Advanced section.
2026-01-27 14:58:37 -07:00
Mbucari d957d6d5d7 Update troubleshooting guide with Hangover app details
Added troubleshooting information for the Hangover app and SQLite Error 10.
2026-01-27 14:53:08 -07:00
Michael Bucari-Tovo cb159336a6 Enable NTRs on main form and fix resulting warnings. 2026-01-27 12:42:21 -07:00
Michael Bucari-Tovo 807bb56c49 Refactor Chardonnay startup to prevent loading library twice. 2026-01-27 12:31:17 -07:00
Mbucari 3d56554aa5 Merge branch 'rmcrackan:master' into master 2026-01-27 12:29:03 -07:00
rmcrackan f720127a68 Merge pull request #1579 from rmcrackan/rmcrackan/bug-1578
Bug #1578 Add null checks
2026-01-27 13:22:20 -05:00
Robert c6c3b39e9c Bug #1578 Adding null checks to the line that's logged with NRE. This bit of code has been untouched for years and is run hundreds of times per day. I'm documenting this suspiciously 2026-01-27 09:06:46 -05:00
rmcrackan e70cc5342c Merge pull request #1572 from shuvashish76/patch-1
Add AppImage installation instructions
2026-01-23 09:45:12 -05:00
shuvashish76 bf0d380dd2 Add AppImage installation instructions
Added installation instructions for AppImage using AppMan and AM.
2026-01-23 14:41:46 +05:30
Michael Bucari-Tovo 471fd1e757 Remove unwanted meta box from dash files 2026-01-22 16:33:12 -07:00
Robert a43f25db23 incr ver 2026-01-22 17:50:31 -05:00
rmcrackan e1f4168599 Merge pull request #1567 from Mbucari/master
Minor big fixes and performance improvements
2026-01-22 17:48:31 -05: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 e9016ace03 Make IncludedUntil and LastDownload nulls always sort to bottom 2026-01-21 13:41:47 -07:00
Michael Bucari-Tovo d498c094bf Update AAXClean
Some minor performance improvements, especially when opening a file/stream.
2026-01-21 13:40:59 -07:00
Michael Bucari-Tovo ce92e79cd8 Fix SaferEnumerateFiles creating too manu open file handles (#1565)
Using the EnumerationOptions overload of EnumerateFiles() serves the same function as the previous manual implementation.
2026-01-21 10:34:59 -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 3ef0bce909 No reason to restrict CDM type to Android or Level to L3 2026-01-19 14:16:47 -07:00
Robert 413da72bf0 logging typo 2026-01-19 10:31:34 -05:00
Robert db9c810c2d incr ver 2026-01-13 20:05:28 -05:00
rmcrackan ae18ae1b8d Merge pull request #1558 from Mbucari/master
Fix export, remove spatial audio option, and add file move progress.
2026-01-13 19:56:24 -05:00
Michael Bucari-Tovo c1298e9ff6 Update dependencies 2026-01-13 12:02:36 -07:00
Michael Bucari-Tovo 647eb8b9d9 Use MemoryPool 2026-01-13 11:35:59 -07:00
Michael Bucari-Tovo fd64d394c2 Remove spatial audio options #1553 2026-01-13 09:43:51 -07:00
Michael Bucari-Tovo f026d415bd Fix library export #1552 2026-01-13 09:35:08 -07:00
Michael Bucari-Tovo 3948e25c99 Add file move progress 2026-01-12 14:34:52 -07:00
Robert 6df47d4d9c incr ver 2026-01-12 16:32:16 -05:00
rmcrackan 0103c879f2 Merge pull request #1551 from Mbucari/master
Update dependencies
2026-01-12 14:17:05 -05:00
Michael Bucari-Tovo 79e2bca8fc Update dependencies 2026-01-12 10:42:04 -07:00
rmcrackan 7bbc681767 Merge pull request #1548 from Mbucari/master
Improved trash bin, new book properties, and bug fixes
2026-01-11 17:54:53 -05:00
Mbucari c95dccd246 Add confirmation dialog when removing books from Audible 2026-01-11 15:45:56 -07:00
Mbucari 94cf665be7 Fix books not being marked absent on large imports 2026-01-11 15:40:11 -07:00
Mbucari 7d38874257 Merge branch 'rmcrackan:master' into master 2026-01-09 16:49:09 -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
MBucari 6f7cd4d5b5 Fix narrators with null ASINs not importing (#1545 ) 2026-01-08 19:57:47 -07:00
Michael Bucari-Tovo 068f37319f Add option to adjust minimum file duration
when splitting audiobooks into multiple files by chapter
2026-01-08 18:36:57 -07:00
Michael Bucari-Tovo dc58a101af Add cli export option to specify Asins 2026-01-08 16:54:43 -07:00
Michael Bucari-Tovo 7b68415b02 Add more properties to search engine and library export
- Add `IsAudiblePlus` to search engine
- Add `IsAudiblePlus` and `AbsentFromLastScan` properties to library export
- Refactor library export ToXlsx method
  - Make nullable
  - Improve readability and extensability
  - Use same column header names as CSV
  - Extend export methods to accept optional list of books (future use)
2026-01-08 15:14:20 -07:00
Michael Bucari-Tovo 1514de54da Add menu option to remove Plus books from Audible 2026-01-08 13:00:47 -07:00
Michael Bucari-Tovo 804bac5c4c Add LibraryBook.IsAudiblePlus property 2026-01-07 15:50:23 -07:00
Michael Bucari-Tovo 3fa805d51f Verify correct Serilog parameters used (#1536 ) 2026-01-07 15:10:54 -07:00
rmcrackan 1eff725125 Merge pull request #1541 from rmcrackan/dependabot/npm_and_yarn/preact-10.28.2
Bump preact from 10.28.0 to 10.28.2
2026-01-07 14:35:44 -05:00
dependabot[bot] af1b1a70ae Bump preact from 10.28.0 to 10.28.2
Bumps [preact](https://github.com/preactjs/preact) from 10.28.0 to 10.28.2.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.28.0...10.28.2)

---
updated-dependencies:
- dependency-name: preact
  dependency-version: 10.28.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-07 19:29:23 +00:00
Robert 042f2e135a incr ver 2026-01-06 07:28:33 -05:00
Robert 930fc3da58 Merge branch 'master' of https://github.com/rmcrackan/Libation 2026-01-06 07:26:40 -05:00
Robert 90e8d03590 add github doc contributor 2026-01-06 07:26:35 -05:00
rmcrackan ee908a4f13 Merge pull request #1534 from Mbucari/master
Improve LocateAudiobooksDialog and fix Classic upgrade dialog
2026-01-06 07:26:08 -05:00
rmcrackan 70ec31303b Merge pull request #1535 from niontrix/patch-1
Document Pacstall for program installation on Linux
2026-01-06 07:16:14 -05:00
Tobias Heinlein 319d547aa0 Document Pacstall for program installation on Linux
Added information about Pacstall for easier program installation.
2026-01-06 10:33:20 +01:00
Mbucari a59e42e7c9 Merge branch 'rmcrackan:master' into master 2026-01-05 18:50:51 -07:00
Michael Bucari-Tovo af2e89dd1e Remove UpgradeNotificationDialog form positioning
Hopefully address #1531
2026-01-05 18:50:28 -07:00
rmcrackan b2c5884e11 Merge pull request #1533 from rmcrackan/rmcrackan/log-friendly2
Rmcrackan/log friendly2
2026-01-05 19:30:43 -05:00
Robert 11d9cdefe2 typo 2026-01-05 19:30:03 -05:00
Robert 54485ae150 LogFriendly 2026-01-05 19:28:50 -05:00
Michael Bucari-Tovo 4bd641ee50 Improve LocateAudiobooksDialog
- Move LocatedAudiobooksViewModel to LibationUiBase
- Refactor Avalonia and Classic displays to use same view model.
- Do scan on background task
2026-01-05 15:38:15 -07:00
Robert 6e56297434 LogFriendly 2026-01-05 14:00:43 -05:00
rmcrackan e1f59eadbd Merge pull request #1529 from rmcrackan/rmcrackan/doc-github-sources
Doc re-directs to include pointers to github sources
2026-01-04 09:49:57 -05:00
Robert cfda065219 Doc re-directs to include pointers to github sources 2026-01-04 09:48:14 -05:00
Robert e3ad4a2c32 incr ver 2026-01-02 16:20:46 -05:00
rmcrackan 2b1c772df7 Merge pull request #1527 from Mbucari/master
Add feature to scan library for higher quality books
2026-01-02 16:02:01 -05:00
Mbucari 9b3e4f8762 Merge branch 'rmcrackan:master' into master 2026-01-02 13:13:12 -07:00
MBucari 396d2c8a95 Address rmcrackan comments and refactor 2026-01-02 13:04:35 -07:00
rmcrackan 91090b74ab Merge pull request #1526 from rmcrackan/rmcrackan/fix-export-columns
Bug fix #1524 - Fix export columns
2026-01-02 09:52:33 -05:00
Robert 32979b5905 Bug fix #1524 - Fix export columns 2026-01-02 09:52:17 -05:00
Michael Bucari-Tovo f6b96fc210 Add feature to scan for better quality audiobooks
Add AccessibleDataGridViewColumn which can apply Accessability names and descriptions from the designer.

Create reusable SortBindingList<T> for basic sorting of data-bound items.
2025-12-31 16:31:52 -07:00
Michael Bucari-Tovo 09e610fe08 Sanitize contributor names (#1518) 2025-12-31 13:09:47 -07:00
Michael Bucari-Tovo e50d8c74de Add UseWindowsForms to csproj 2025-12-31 11:28:45 -07:00
Michael Bucari-Tovo 2b1ca13249 Prevent migrations from running more than once 2025-12-31 11:25:43 -07:00
Michael Bucari-Tovo 7d30a3036d Move viewmodel into UiBase 2025-12-30 15:56:47 -07:00
Michael Bucari-Tovo bb8b435810 Improve Find Better Quality Audiobooks scanner
Try to load best audio format from actual liberated audiobook files
Allow re-scanning after completing scanning.
2025-12-30 14:49:58 -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 29a5c943cb Auto-scroll process queue 2025-12-29 21:52:36 -07:00
Mbucari 31087c0855 Add feature to scan library for higher quality books 2025-12-29 19:30:47 -07:00
rmcrackan c91d359017 Merge pull request #1517 from Mbucari/master
Fix mp3 conversion of liberated AC-4 files and add metadata tags
2025-12-27 22:21:10 -05:00
MBucari 7dfdc0688a Add some more useful tags
AUDIBLE_ACR, AUDIBLE_DRM_TYPE, and AUDIBLE_LOCALE
2025-12-27 15:40:57 -07:00
MBucari c6c36c74f1 Allow converting already downloaded AC-4 to mp3 2025-12-27 14:40:10 -07:00
rmcrackan d932b57853 Merge pull request #1512 from radiorambo/fix-1508
Fix 1508 | auto redirect old docs links to new docs, add new development section to docs
2025-12-27 09:47:04 -05:00
radiorambo a29da7318b remove routex packgage 2025-12-27 13:04:37 +05:30
radiorambo 678c3e6bcd add Documentation folder with old pages but with only link to new docs 2025-12-27 13:03:55 +05:30
rmcrackan a8466e38d4 Merge pull request #1515 from Mbucari/master
Add CFBundleShortVersionString to Info.plist
2025-12-26 16:41:03 -05:00
Mbucari 802ccf25e8 Add recent contributors 2025-12-26 14:08:59 -07:00
Mbucari c243b9c913 Add CFBundleShortVersionString to Info.plist 2025-12-26 13:41:38 -07:00
radiorambo 4d47ab3ebe update old docs links 2025-12-25 15:53:01 +05:30
Umesh 8dea6200ce Merge branch 'master' into fix-1508 2025-12-24 13:38:24 +05:30
radiorambo a578777352 fix broken links 2025-12-24 11:17:43 +05:30
radiorambo e58e5165cf update readme 2025-12-24 11:17:29 +05:30
radiorambo 87c2cb6e19 add separete section for development related 2025-12-24 11:11:48 +05:30
Robert 4f44c26b57 incr ver 2025-12-23 09:52:25 -05:00
rmcrackan 03534773ab Merge pull request #1510 from rmcrackan/dependabot/github_actions/actions/configure-pages-5
Bump actions/configure-pages from 4 to 5
2025-12-23 09:39:59 -05:00
rmcrackan 37f223fb77 Merge pull request #1509 from rmcrackan/dependabot/github_actions/actions/upload-pages-artifact-4
Bump actions/upload-pages-artifact from 3 to 4
2025-12-23 09:31:01 -05:00
radiorambo cf932bd66c redirect old doc links to new doc links 2025-12-23 19:58:14 +05:30
rmcrackan f0dc33a01e Merge pull request #1511 from rmcrackan/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-12-23 09:11:11 -05:00
dependabot[bot] 315d76e061 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 14:06:01 +00:00
dependabot[bot] 6e78145adc Bump actions/configure-pages from 4 to 5
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 4 to 5.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 14:05:51 +00:00
dependabot[bot] 200a334f86 Bump actions/upload-pages-artifact from 3 to 4
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 14:05:45 +00:00
rmcrackan 4dd4a1495a Update docker.md 2025-12-22 17:46:10 -05:00
rmcrackan b3ce0e0af0 Update docker.md
Add LIBATION_CONNECTION_STRING
2025-12-22 17:45:15 -05:00
Mbucari 1299d91d08 Restrict workflow runs to specific paths 2025-12-22 12:54:58 -07:00
rmcrackan ad3a767057 Merge pull request #1504 from Youssef1313/mtp
Migrate from VSTest to MTP
2025-12-22 14:45:19 -05:00
Michael Bucari-Tovo a59c73caf8 Add win-arm64 release identfier 2025-12-22 12:37:22 -07:00
rmcrackan 442a688b85 Update README.md 2025-12-22 10:28:21 -05:00
rmcrackan 0c85ea4d11 Merge pull request #1498 from radiorambo/new-documentation-website
add new documentation website
2025-12-22 09:50:45 -05:00
Youssef1313 03ed8e6b57 Migrate from VSTest to MTP 2025-12-21 15:03:52 +01:00
rmcrackan 3eca508a26 Merge pull request #1501 from Mbucari/master
Add support for decoding Windows Arm64 and AC-4 audio files
2025-12-19 08:21:26 -05:00
radiorambo 770adf33f3 add gitHub actions workflow for vitePress deployment to gitHub pages. 2025-12-19 14:09:21 +05:30
MBucari 1087ffb150 Add support for converting AC-4 files to mp3 2025-12-19 00:18:06 -07:00
radiorambo f620234e7d add docs overview in homepage and in nav bar links 2025-12-18 14:28:46 +05:30
radiorambo 2b6b5d082e fix nav link increase website width in tablet view 2025-12-18 14:27:17 +05:30
Michael Bucari-Tovo cbbc45c3c5 Add Windows arm64 build 2025-12-17 11:00:29 -07:00
rmcrackan 28de1a6cb6 Merge pull request #1500 from Mbucari/master
Update AAXClean
2025-12-17 06:45:13 -05:00
Michael Bucari-Tovo 1615c6ef77 Update AAXClean 2025-12-16 23:44:04 -07:00
radiorambo 6961bd72fa rename 'report issues' button to 'issues & requests' and simple installation routes 2025-12-16 13:22:56 +05:30
radiorambo 68846a90e5 fix dead links 2025-12-16 13:11:56 +05:30
radiorambo d60ec0702c update nav and homepage buttons 2025-12-16 13:07:55 +05:30
radiorambo 1c55c8533a improve docs 2025-12-16 13:06:44 +05:30
radiorambo 6fa69b603e rename files 2025-12-15 22:08:01 +05:30
radiorambo 3df8a97463 configure config for clean urls 2025-12-15 21:36:39 +05:30
rmcrackan 0bd7bd80b9 Merge pull request #1496 from rmcrackan/dependabot/github_actions/actions/download-artifact-7
Bump actions/download-artifact from 6 to 7
2025-12-15 09:17:03 -05:00
rmcrackan 13bb4238b4 Merge pull request #1497 from rmcrackan/dependabot/github_actions/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6
2025-12-15 09:16:49 -05:00
dependabot[bot] d5021e4f74 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 14:06:46 +00:00
dependabot[bot] 5e1458cfb4 Bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 14:06:41 +00:00
radiorambo e1d4533887 use github syntax for admonitions in homepage 2025-12-15 18:27:37 +05:30
radiorambo c1bd1d983b make documentation development section in readme concise 2025-12-15 17:50:39 +05:30
radiorambo b567c38a98 update syntax, adjust heading levels, reformat tables, and apply minor text and capitalization fixes across documentation. 2025-12-15 17:45:34 +05:30
radiorambo 348ec22465 update package-lock.json 2025-12-15 17:43:46 +05:30
radiorambo 90bb4d9176 fix favicon and logo not visible 2025-12-15 16:05:02 +05:30
radiorambo 7944154ea6 add icons 2025-12-15 15:37:19 +05:30
radiorambo 01fc7f3fb9 rename documentation files for simple routing paths and reorganise for better navigation 2025-12-15 15:37:07 +05:30
radiorambo b70f973994 update .gitignore for vitepress 2025-12-15 15:34:37 +05:30
radiorambo 98d3f85579 install vitepress and configure 2025-12-15 15:28:05 +05:30
Robert bdae155af6 incr ver 2025-12-11 16:41:36 -05:00
rmcrackan c8b44193ac Merge pull request #1490 from Mbucari/master
Two bugfixes
2025-12-09 13:39:14 -05:00
Mbucari 9545b3a874 Invoke MessageBox on UI thread 2025-12-06 18:55:38 -07:00
Mbucari e932c9fab9 Merge branch 'rmcrackan:master' into master 2025-12-06 18:02:38 -07:00
Robert c8f4c1e751 Merge branch 'master' of https://github.com/rmcrackan/Libation 2025-12-06 14:48:26 -05:00
Robert 0303db153f update audibleapi 2025-12-06 14:48:24 -05:00
Mbucari a7e9479eab Fix file utility modifying file extension using replacement character
The file extension should not be subject to renaming. #1483
2025-12-06 10:40:02 -07:00
Mbucari d339dbc906 Update macOS install instructions. 2025-12-05 21:06:02 -07:00
Robert 5fe6f931ad incr ver 2025-12-04 20:54:08 -05:00
rmcrackan ca9fe9fc32 Merge pull request #1479 from Mbucari/master
Two minor bug fixes
2025-12-04 20:52:19 -05:00
MBucari 986dbd678f Don't throw exceptions from failure to delete db-wal and db-shm files (#1478) 2025-12-03 22:09:35 -07:00
MBucari ea3716f48a Fix books dialog not saving or updating properly (#1477) 2025-12-03 22:03:14 -07:00
rmcrackan 426d5a87b4 Merge pull request #1476 from rmcrackan/dependabot/github_actions/apple-actions/import-codesign-certs-6
Bump apple-actions/import-codesign-certs from 5 to 6
2025-12-03 09:48:46 -05:00
dependabot[bot] c893bbe52e Bump apple-actions/import-codesign-certs from 5 to 6
Bumps [apple-actions/import-codesign-certs](https://github.com/apple-actions/import-codesign-certs) from 5 to 6.
- [Release notes](https://github.com/apple-actions/import-codesign-certs/releases)
- [Commits](https://github.com/apple-actions/import-codesign-certs/compare/v5...v6)

---
updated-dependencies:
- dependency-name: apple-actions/import-codesign-certs
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 14:06:28 +00:00
Robert ad5a9874af incr ver 2025-12-02 15:28:59 -05:00
rmcrackan 3b70c08439 Merge pull request #1471 from Mbucari/master
Classic dark mode, customizable downloads, bug fixes
2025-12-02 15:26:24 -05:00
Michael Bucari-Tovo a230605ed5 Improve GetCounts performance. 2025-12-02 12:43:12 -07:00
Michael Bucari-Tovo d48ce39773 Move LibraryCommands.GetCounts() to background thread. 2025-12-02 12:30:05 -07:00
Michael Bucari-Tovo 368e695214 Allow users to shoose whether Libation imports Audible Plus titles. 2025-12-02 12:28:16 -07:00
Michael Bucari-Tovo 9c3881c67d Fix LibationCli reusing content licenses (#1473) 2025-12-02 11:09:58 -07:00
MBucari 4c5fdf05f5 Add "Download split by chapters" context menu item (#1436)
All processables are now created with an instance of Configuration, and they use that instance's settings.

Added Configuration.CreateEphemeralCopy() to clone Configuration without persistence.
2025-12-01 23:23:47 -07:00
MBucari 4bd491f5b9 Make winforms book details and search syntax dialogs nonmodal
Match Chardonnay behavior
2025-12-01 21:24:30 -07:00
MBucari c34b1e752e Update dependencies 2025-12-01 20:40:34 -07:00
MBucari fa30c10435 Fix PDF validation error (#1470 ) 2025-12-01 20:39:52 -07:00
Michael Bucari-Tovo cdb91ae2ca Add dark mode to winforms
- Add dark theme icon variants
- Change all light theme icon fill colors to match Chardonnay

Also fixed #1460  by chaing the directory select control to DirectoryOrCustomSelectControl
2025-12-01 20:39:22 -07:00
Robert 7852067b81 incr ver 2025-11-29 23:22:42 -05:00
rmcrackan 3708515df9 Merge pull request #1467 from Mbucari/master
Fix MessageBox launch error on macOS
2025-11-29 23:21:14 -05:00
Mbucari 530aca4f4d Fix MessageBox launch error on macOS 2025-11-29 15:41:38 -07:00
Robert cf571148bc incr ver 2025-11-25 23:19:49 -05:00
rmcrackan 2c2a720ba9 Merge pull request #1458 from Mbucari/master
Bug fixes in the downloader
2025-11-25 22:47:48 -05:00
Michael Bucari-Tovo b577ef7187 Improve SetBeckupCounts
Change Avalonia's Task-based approach to WinForms' BackgroundWorker approach.
- Reduce number of calls to GetLibrary by adding the Library to the LibraryStats record.
2025-11-25 14:59:48 -07:00
Michael Bucari-Tovo ffbb3c3516 Make namespace name match assembly name 2025-11-25 13:34:12 -07:00
Michael Bucari-Tovo 2a6cf38677 Fix book details dialog not saving 2025-11-25 13:33:40 -07:00
Michael Bucari-Tovo d8104a4d7c Check is stream is disposed before reading position. 2025-11-25 12:34:20 -07:00
Michael Bucari-Tovo af85ea9219 Fix exception being throw in Dispose() 2025-11-25 12:24:07 -07:00
rmcrackan c30e149a36 Merge pull request #1456 from Mbucari/master
Fix database lock from -wal and -whm files
2025-11-24 22:57:12 -05:00
MBucari 050a4867b7 Fix database lock from -wal and -whm files
Delete LibationContext.db-shm and LibationContext.db-wal files as part of startup routine.
2025-11-24 20:45:55 -07:00
Robert 2bf6f7a4f2 incr ver 2025-11-24 15:46:01 -05:00
rmcrackan 788a768271 Merge pull request #1455 from Mbucari/master
Add liberate option `--license` to use license file.
2025-11-24 15:43:18 -05:00
Michael Bucari-Tovo 022a6e979d Add error handling for cookies 2025-11-24 12:01:10 -07:00
Michael Bucari-Tovo 9fc5a7d834 Add liberate option --license to use license file.
Added instructions for liberating using a license file.
2025-11-24 11:36:31 -07:00
Robert b72e5039b1 incr ver 2025-11-24 09:48:22 -05:00
rmcrackan e992b49da2 Merge pull request #1451 from Mbucari/master
Add more liberation logging
2025-11-24 09:14:23 -05:00
MBucari 74afbbf581 Add more liberation logging 2025-11-23 23:40:23 -07:00
rmcrackan d82ffe1467 Merge pull request #1444 from Mbucari/master
Use C# 14 `field` and extension members.
2025-11-23 22:44:57 -05:00
Mbucari 8a84a083d1 Fix removed field 2025-11-23 20:38:20 -07:00
Mbucari 04827f81da Tweak in-app web browser login
- Use private browsing mode
- Use the Android User-Agent
- Use initial signin cookies
2025-11-23 20:35:04 -07:00
Mbucari 805f42b1cc Add warnings for inaccessable InProgress directory (#1446) 2025-11-22 15:29:54 -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
Michael Bucari-Tovo dfbc5ec9db Use the new field keyword where appropriate. 2025-11-21 11:50:07 -07:00
rmcrackan 649ef5f864 Merge pull request #1441 from rmcrackan/dependabot/github_actions/apple-actions/import-codesign-certs-5
Bump apple-actions/import-codesign-certs from 3 to 5
2025-11-21 11:19:27 -05:00
rmcrackan 4345bf2ee2 Merge pull request #1442 from rmcrackan/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-11-21 11:19:09 -05:00
rmcrackan 441d430dea Merge pull request #1439 from Mbucari/master
Update to .NET 10
2025-11-21 09:15:00 -05:00
dependabot[bot] 85ee0bcddb Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 14:06:00 +00:00
dependabot[bot] 0f1fc0f11d Bump apple-actions/import-codesign-certs from 3 to 5
Bumps [apple-actions/import-codesign-certs](https://github.com/apple-actions/import-codesign-certs) from 3 to 5.
- [Release notes](https://github.com/apple-actions/import-codesign-certs/releases)
- [Commits](https://github.com/apple-actions/import-codesign-certs/compare/v3...v5)

---
updated-dependencies:
- dependency-name: apple-actions/import-codesign-certs
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 14:05:53 +00:00
MBucari 75aa17df11 Fix winforms STAThread on main 2025-11-20 22:35:13 -07:00
MBucari 913019cdfd Revert hack workaround for DataGridView error 2025-11-20 22:19:43 -07:00
MBucari a55da5f187 Refactor DbContext access and disposal
- Remove instance queue. This is a database, after all, and is designed to be accessed and written to concurrently
- Reduce the number of calls to DbContexts.Create()
- Ensure that no LibationContext remains open across an await boundary. Multithread context access is the most likely culprit for past issues.
- Make all Update UserDefinedItem methods asynchronous.
2025-11-20 22:15:54 -07:00
MBucari f9ac0253fb Improve import performance on large batches 2025-11-20 21:37:13 -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
rmcrackan 22159b79d8 Merge pull request #1429 from Mbucari/master
Minor Bugfixes, new Workflows, and signed macOS bundles, refactor LibationFiles, improve libationCLI
2025-11-20 13:45:25 -05:00
MBucari 7fe170acdf Fix CLI help tex being squashed to a single column 2025-11-20 10:42:14 -07:00
MBucari c0898a288b Additional LibationCli documentation
Move CLI examples to the bottom of the page.
2025-11-20 09:55:35 -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 65d24ce223 Fix NRE in FileSystemTest when Books dir is invalid (#1435) 2025-11-17 10:54:25 -07:00
MBucari e8c911e603 Improve management and validation of Libation settings
- Move all settings file logic into new LibationFiles class
  - Configuration.LibationFiles is a singleton instance of the LibationFiles class.
  - A LibationFiles instance is bound to a single appsettings.json path. All updates to LibationFiles location are updated in that appsettings.json file
- Unify initial setup and settings validation process
  - Add LibationSetup which handles all startup validation of settings files and prompts users for setup if needed
  - Added a new LibationUiBase.Tests test project with tests for various
2025-11-17 10:49:23 -07:00
Michael Bucari-Tovo 59f66ff480 Fix subdirectory create failing on root drive (#1432)
This appears to be a bug in .NET. Someone started to fix it in a PR, but it went stale and was auto-closed.

https://github.com/dotnet/runtime/pull/117519
2025-11-14 13:44:46 -07:00
Michael Bucari-Tovo e05dcd6f54 Don't overwrite LibationFiles setting in appsettings.json 2025-11-14 13:35:36 -07:00
Michael Bucari-Tovo d1ce9d5a83 Update Mac Workflow
- Add new repo variables
  - `SIGN_MAC_APP_ON_VALIDATE` will force sign/notarize on the validate workflow (normally only done for releases)
  - `WAIT_FOR_NOTARIZE` Causes the build-mac workflow to wait for apple to notarize the bundle so that it can be stapled. This is usually fast (1-2 mis), but can be very long and may cause workflow runners to time out.
2025-11-14 11:19:21 -07:00
Mbucari 2213f5c86a Change msaOS updater to get DMGs
This will break _Automatic_ updates for existing mac users (although I'm not sure it worked all that well to begin with. However, the update notification dialog has had a link to download the bundle manually for a long time now. Old users will still be notified of the new release and be given a direct link to download it.
2025-11-13 23:15:29 -07:00
Mbucari d6b232f342 Update Logging and Error Handling
- Add Configuration.LoggingEnabled property which gets set as soon as Serilog is configured
- Add error handling to InteropFactory
2025-11-13 23:12:57 -07:00
Mbucari f29c19beb8 Update .gitignore 2025-11-13 23:04:33 -07:00
Mbucari 9f6d08fc1f Update Workflows
- Simplify workflows build commands
- Don't build ReadyToRun on validate
- Move get-version into it's own job in build.yml
- Split  macOS into it's own reusable workflow
  - Add app bundle code signing
  - Add notarization
2025-11-13 22:59:26 -07:00
MBucari 717dfcd923 Fix trying to cancel disposed cancellation token source 2025-11-11 14:06:54 -07:00
MBucari a3d181b2ec Fix Primary Screen NRE (#1420) 2025-11-11 03:21:20 -07:00
Mbucari d16eeea56b Improve detection of NativeWebDialog crash 2025-11-10 22:58:05 -07:00
Mbucari 5d2513ec33 Improve button display size uniformity. 2025-11-10 22:21:51 -07:00
Mbucari e5043dcf40 Move button styling to App.xaml 2025-11-10 21:42:34 -07:00
Mbucari c61bfb4134 Fix EditQuickFilters not displaying with no filters 2025-11-10 21:42:06 -07:00
Robert d47a2595b9 incr ver 2025-11-10 22:19:24 -05:00
rmcrackan 55e74db4fb Merge pull request #1419 from Mbucari/master
Bug Fixes and UI Improvement
2025-11-10 22:17:12 -05:00
MBucari 0a171222bc Update Dependency 2025-11-10 19:29:30 -07:00
MBucari c2093157ca Add dolby atmos logo for spatial audiobooks 2025-11-10 19:28:18 -07:00
MBucari 8e073800cd Fix BookDetailsDialog crash when changing error status 2025-11-10 18:25:59 -07:00
MBucari 1daf07b882 Improve logging 2025-11-10 17:58:48 -07:00
MBucari 27a23a16d6 Update AAXClean 2025-11-10 17:34:17 -07:00
Michael Bucari-Tovo c878b9fec0 Detect webview crash and disable webview login 2025-11-10 13:14:23 -07:00
rmcrackan 7a01f075ac Merge pull request #1415 from Mbucari/master
Fix minor UI bugs
2025-11-08 18:04:13 -05:00
Michael Bucari-Tovo 23d391485d Update AboutDialog and add recent contributors 2025-11-07 10:35:33 -07:00
Michael Bucari-Tovo 46be532740 Improve SearchSyntaxDialog
- Double-clicking a tag will paste the tage into the search bar
- SearchSyntaxDialog now modeless
2025-11-06 23:53:57 -07:00
Michael Bucari-Tovo e2fd88d075 Improve ScanAccountsDialog usability 2025-11-06 23:24:17 -07:00
Michael Bucari-Tovo bb0dea3fa9 Improve EditReplacementChars dialog usability 2025-11-06 22:49:09 -07:00
Michael Bucari-Tovo def0b1f611 Prevent crash if watched RootDirectory is deleted 2025-11-06 14:57:54 -07:00
Michael Bucari-Tovo bfee579719 Fix DirectoryOrCustomSelectControl 2025-11-06 13:47:51 -07:00
Mbucari d4139861f3 Only allow mocking lobby bugging 2025-11-06 07:59:55 -07:00
Robert ba15eb1a95 incr ver 2025-11-06 09:43:11 -05:00
rmcrackan 6263fedf84 Merge pull request #1406 from Mbucari/master
Improved Login experience, error messages, and published size.
2025-11-06 08:44:44 -05:00
MBucari 0cbffc3f6c Only allow mocking settings while debugging 2025-11-05 23:52:44 -07:00
MBucari 5f093b06ec Improve Chardonnay setup reliability. 2025-11-05 23:40:20 -07:00
Michael Bucari-Tovo f815c5fd47 Define context menu in XAML, remove need for reflection 2025-11-05 16:20:47 -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 01b5c18b2b Improve Column Context Menu CheckBox display 2025-11-05 09:39:32 -07:00
Michael Bucari-Tovo 5634fee2aa Add Account column #1398 2025-11-05 08:48:29 -07:00
MBucari e98e4f10bc Ensure FileSystemWatcher is disposed 2025-11-04 22:08:36 -07:00
MBucari ec32ff77b2 Fix theme not being applied when changed by the system (#1368) 2025-11-04 22:07:29 -07:00
Michael Bucari-Tovo 683c984246 Modify script which populates username in webview 2025-11-04 15:06:03 -07:00
Michael Bucari-Tovo 0fa5c4eb1e Request metadata for the audiobook version being downloaded (#1261) 2025-11-04 14:58:27 -07:00
Michael Bucari-Tovo 7507044b82 Improve detection and logging of download capabilities
- Check if the Api supports widevine before trying to download
- Additional logging
2025-11-04 14:32:28 -07:00
Michael Bucari-Tovo 017902ab52 Click to open libation log file 2025-11-04 13:19:40 -07:00
Mbucari dcc5c1c640 Fix quotes in Libation.desktop Exec command 2025-11-03 18:32:05 -07:00
Michael Bucari-Tovo 19efa8c918 Improve error messages for Chardonnay
- A message box alert should be possible regardless of the error
- If crash pre-logging, attempt to write to last used Libation log file
2025-11-03 17:40:38 -07:00
Michael Bucari-Tovo a34efb5e61 Add multiple image sizes in windows folder icons 2025-11-03 14:04:30 -07:00
Michael Bucari-Tovo 9533f80e89 Replace NPOI excel workbook library with ClosedXML
- Reduce build bundles by 30-40 MB
2025-11-03 13:13:13 -07:00
Michael Bucari-Tovo fa238a0915 Use xplat webview control for Audible login
- Use Avalonia-based webview control for Audible login with Chardonnay
- Remove webview interfaces from IInteropFunctions
- Remove Microsoft.Web.WebView2 package from WindowsConfigApp
- Add Microsoft.Web.WebView2 to LibationWinForms
- Remove all other login forms except the external login dialog (fallback in case webview doesn't work). The AudibleApi login with username/password doesn't work anymore. Need to use external browser login method.
2025-11-03 11:29:57 -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
rmcrackan d85e5a0f98 Update Advanced.md
CLI: copy the local sqlite database to postgres
2025-11-03 11:24:02 -05:00
Robert 365ac8167f incr ver 2025-11-03 11:04:55 -05:00
Robert 4720779373 Bug fix. DesignTimeDbContextFactoryBase used to call context.Database.Migrate() . Must now do so ourselves 2025-11-03 11:00:10 -05:00
Robert 0c512162ab Reverting migration notes from #1402 2025-11-03 10:21:13 -05:00
rmcrackan 09ca419faf Merge pull request #1402 from twsouthwick/postgres
Add support for postgres
2025-11-03 09:47:48 -05:00
Taylor Southwick a2b1f13601 remove migration calls 2025-11-02 18:32:20 -08:00
Robert f4e7cf3418 Bugfix #1403 : Trash bin actions result in app crashes 2025-11-01 13:32:19 -04:00
MBucari 8492a7ea3a Properly disposed of LibationContext (#1403) 2025-10-31 13:45:07 -06: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
rmcrackan 5589a6cbd5 Merge pull request #1401 from rmcrackan/dependabot/github_actions/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-10-27 11:22:19 -04:00
dependabot[bot] bfbad988c0 Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 15:14:03 +00:00
rmcrackan 8db8615713 Merge pull request #1400 from rmcrackan/dependabot/github_actions/actions/download-artifact-6
Bump actions/download-artifact from 5 to 6
2025-10-27 11:07:00 -04:00
dependabot[bot] e8fa3f14b3 Bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 14:59:43 +00:00
Robert 71552f2417 incr ver 2025-10-23 09:45:55 -04:00
rmcrackan 5b96f96a80 Merge pull request #1390 from Mbucari/master
Bug Fixes and attempts to solve random crashes
2025-10-23 09:43:23 -04:00
Michael Bucari-Tovo afffeb953c Enforce sequential access to DbContext. 2025-10-22 09:22:05 -06:00
Mbucari f4d8685058 Merge branch 'rmcrackan:master' into master 2025-10-22 09:16:49 -06:00
Robert b4cfd18976 incr ver 2025-10-20 21:12:43 -04:00
rmcrackan e12548dacd Merge pull request #1388 from delebash/master
Added IncludedUntil Date
2025-10-20 21:10:46 -04:00
delebash fd95ac7a9c changes per Mbucari 2025-10-20 16:31:06 -04: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
delebash fcd79c5561 InludedUntil fixes by Mbucari 2025-10-20 12:55:48 -04:00
delebash ba98820989 move code to LibraryBook 2025-10-18 02:05:56 -04:00
delebash b07e61e6a8 fix UntilDate 2025-10-17 13:49:37 -04:00
delebash 8c3fd19c70 Merge remote-tracking branch 'origin/master' 2025-10-17 13:36:21 -04:00
delebash fa8f761771 Added IncludedUntil Date 2025-10-17 13:36:01 -04:00
rmcrackan 2c882e883d #1378 : allow for invalid beginning-of-time 'purchase_date'. This is the case for an actual user 2025-09-30 16:28:48 -04:00
Mbucari 74c76a7414 Enhance bug report template formatting and instructions
Updated the bug report template to improve clarity and formatting, including default log file locations for various platforms.
2025-09-04 12:46:22 -06:00
Mbucari 17a0c21453 Document xHE-AAC conformance errors for Audible
Added notes on xHE-AAC conformance errors related to Audible files.
2025-09-04 12:24:46 -06:00
rmcrackan fc9c9dfe48 Merge pull request #1360 from rmcrackan/dependabot/github_actions/actions/setup-dotnet-5
Bump actions/setup-dotnet from 4 to 5
2025-09-03 21:43:10 -04:00
dependabot[bot] d5f0e39981 Bump actions/setup-dotnet from 4 to 5
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 00:05:35 +00:00
rmcrackan 0f6493f4af Merge pull request #1353 from rmcrackan/rmcrackan-patch-2
Update NamingTemplates.md
2025-08-27 09:30:09 -04:00
rmcrackan 454b490a06 Update NamingTemplates.md 2025-08-27 09:28:45 -04:00
rmcrackan ffea2648aa Merge pull request #1352 from rmcrackan/rmcrackan-patch-1
Update NamingTemplates.md
2025-08-27 09:22:20 -04:00
rmcrackan 1ac967500c Update NamingTemplates.md
Better documentation for inverted tags
2025-08-27 08:57:20 -04:00
Michael Bucari-Tovo 39e9f675d2 Fix possible race error on startup with autoscan 2025-08-26 10:00:21 -06:00
rmcrackan ed5afe5d0f update dependencies 2025-08-20 12:07:53 -04:00
rmcrackan ab075d0bef Merge pull request #1343 from MajorTanya/patch-1
Place examples in their own line
2025-08-20 11:49:46 -04:00
MajorTanya 7fb1adb41b Place examples in their own line
Markdown collapses single line breaks, so this change makes it so the examples will have their own lines.
2025-08-20 17:26:48 +02:00
rmcrackan 9735a8391c incr ver 2025-08-20 08:39:10 -04:00
rmcrackan dbdfdbc536 Merge pull request #1342 from Mbucari/master
Added new <has PROPERTY-><-has> conditional tag
2025-08-20 08:37:11 -04:00
Michael Bucari-Tovo 3b86fc405f Add <has-> template tag 2025-08-19 18:41:31 -06:00
MBucari 4ea7f04921 Preserve space between series order numbers 2025-08-17 13:40:37 -06:00
MBucari 5b59b442ab Add last downloaded sample rate, bitrate and codec name to search engine. 2025-08-17 13:07:24 -06:00
rmcrackan b5d9c0a27a Incr ver 2025-08-17 10:06:12 -04:00
rmcrackan f5cbf89e13 Merge pull request #1337 from Mbucari/master
Fix linux crpto and series order parsing
2025-08-17 09:57:43 -04:00
rmcrackan 00dc9e020d Update bug_report.md 2025-08-17 09:55:26 -04:00
MBucari bfa0e4d338 Parse floats with invariant culture 2025-08-16 16:39:36 -06:00
Mbucari 5ceda408da Use managed RSASSA-PSS with SHA-1
OpenSSL (the underlying RSA implementation on Linux) has deprecated SHA-1 signing. Used a managed implementation so that it does not error.
2025-08-16 16:28:33 -06:00
Mbucari 716b1923a4 Update FrequentlyAskedQuestions.md 2025-08-15 12:25:03 -06:00
rmcrackan 1148d8125d incr ver 2025-08-15 13:10:05 -04:00
rmcrackan 690fd10e42 Merge pull request #1331 from Mbucari/master
Audio format docs, new audio format options, series order parsing.
2025-08-15 13:08:10 -04:00
Michael Bucari-Tovo 736fbbf82f Improve FilePathCache performance 2025-08-15 10:50:37 -06:00
Michael Bucari-Tovo eda100b7ac Remove FluentAssertions 2025-08-15 10:29:23 -06:00
Michael Bucari-Tovo ceb007500d Update assertions to use ThrowsExactly 2025-08-14 15:58:01 -06:00
Michael Bucari-Tovo 05fad01624 Update dependencies 2025-08-14 15:57:35 -06:00
Michael Bucari-Tovo e1d789ccdc Improve series order parsing and formatting 2025-08-14 15:37:53 -06:00
Michael Bucari-Tovo d0f00f3f1e Add xHE-AAC option 2025-08-14 13:20:01 -06:00
Michael Bucari-Tovo 6ab82dba7b Add audio format info to wiki 2025-08-14 13:16:27 -06:00
rmcrackan 0045202334 update dependencies 2025-08-13 07:52:13 -04:00
rmcrackan 4c80813651 Merge pull request #1330 from rmcrackan/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-11 17:38:40 -04:00
dependabot[bot] 6b637b35ab Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 20:35:21 +00:00
rmcrackan 9b55ffa715 Merge pull request #1329 from rmcrackan/rmcrackan/dolby-atmos
Begin documentation for Dolby Atmos
2025-08-11 09:22:03 -04:00
rmcrackan 65da7890f1 Begin documentation for Dolby Atmos 2025-08-11 09:21:03 -04:00
rmcrackan 72f92ec6c0 update dependencies 2025-08-10 11:21:34 -04:00
rmcrackan 4efc084375 Merge pull request #1324 from rmcrackan/dependabot/github_actions/actions/download-artifact-5
Bump actions/download-artifact from 4 to 5
2025-08-06 10:24:33 -04:00
dependabot[bot] f955daa5ed Bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-06 14:09:36 +00:00
rmcrackan 144ab2162a incr ver 2025-08-05 12:58:45 -04:00
rmcrackan 6d0c4a9b3c Merge pull request #1322 from Mbucari/master
Improve Libation's interaction with the file system & other minor fixes
2025-08-05 12:57:03 -04:00
Michael Bucari-Tovo 8a682533c1 Change repo link to rmcrackan 2025-08-05 10:39:03 -06:00
Michael Bucari-Tovo cecabc911e Add "Locate Audiobooks" help text 2025-08-05 10:29:48 -06:00
Michael Bucari-Tovo e35f5209dc Don't change user's replacement character settings
Instead, add the NTFS-only invalid characters to the set of invalid filename characters.
2025-08-05 09:49:22 -06:00
MBucari 4ffe70af0e Fix serilog not logging caller name 2025-08-04 23:24:55 -06:00
MBucari 233ba3184f Add link to naming template wiki 2025-08-04 21:23:50 -06:00
Michael Bucari-Tovo ac4c168725 Allow Libation to start with an invalid Books directory
- Configuration.LibationSettingsAreValid is true if Books property exists and is any non-null, non-empty string.
- If LibationSettingsAreValid is false, Libation will prompt user to set up Libation.
- When the main window is shown, Libation checks if the books directory exists, and if it doesn't, user is notified and prompted to change their setting
- When a user tries to liberate or convert a book, Books directory is validated and user notified if it does not exist.
2025-08-04 19:58:26 -06:00
Michael Bucari-Tovo db588629c0 Null safety and minor UI bugfix
Properly cancel the Locate Audiobooks when the dialog window closes before scanning is finished.
2025-08-04 17:15:37 -06:00
Michael Bucari-Tovo 29be091a4b Fix cross-thread error on AccoundSettings.Saved event 2025-08-04 14:18:04 -06:00
Michael Bucari-Tovo 82a48db57b Fix walkthrough errors on chardonnay. 2025-08-04 10:27:37 -06:00
Mbucari 9f0f32a462 Merge branch 'rmcrackan:master' into master 2025-08-04 10:02:02 -06:00
rmcrackan f64239b5ee Merge pull request #1316 from ajundi/master
Added nix flake and shell files for nixos developers
2025-08-04 09:55:42 -04:00
Ayman Jundi bc8a35aedd Added nix flake and shell files for nixos users who want to work on development. You can use flake method => 'nix develop' or non flake method =>`nix-shell'
The shell.nix file is used for both flake and non-flake invocations. The lock file is also set at a version where the project works.
Note the none-flake method will follow the version of the system and isn't guaranteed to work on older installations if they haven't been updated in a while.
Added Documentation for using Nix package manager for development ./Documentation/LinuxDevelopmentSetupUsingNix.md

Signed-off-by: Ayman Jundi <ajundi@gmail.com>
2025-08-03 16:36:03 -04:00
Ayman Jundi 2fca6b8b91 Added launch.json and task.json entries that allows building and debugging for linux-64 targets without having to modify the csproj files.
Signed-off-by: Ayman Jundi <ajundi@gmail.com>
2025-08-03 15:33:17 -04:00
Mbucari bc2eddd2dd Merge branch 'rmcrackan:master' into master 2025-07-31 10:36:26 -06:00
Michael Bucari-Tovo ae012548bd Smart handling of filename limitations cross platform
Automatically determine if filename lengths in the Books directory are limited to 255 UTF-16 characters (NTFS) or 255 UTF-8 bytes (pretty much every other file system) (#1260)

In non-Windows environments, determine if the Books directory supports filenames containing characters which are illegal in Windows environments (<>|:*?). If it doesn't, then ensure those characters are included in the user's ReplacementCharacters settings (#1258).
2025-07-30 16:04:48 -06:00
rmcrackan 76a59873ea undo dependabot overreach 2025-07-30 09:42:43 -04:00
rmcrackan 3129fdba7b Merge pull request #1319 from rmcrackan/dependabot/nuget/Source/ApplicationServices/multi-ab24e9613a
Bump SixLabors.ImageSharp to 2.1.11, 3.1.11
2025-07-30 09:37:18 -04:00
dependabot[bot] 1771813849 Bump SixLabors.ImageSharp to 2.1.11, 3.1.11
---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-version: 2.1.11
  dependency-type: direct:production
- dependency-name: SixLabors.ImageSharp
  dependency-version: 3.1.11
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 13:30:26 +00:00
Michael Bucari-Tovo 7024bbf823 Provide NTFS default characters for non-windows users (#1258) 2025-07-29 15:20:52 -06:00
Michael Bucari-Tovo 663f70b8bf Use series Order string instead of parsed float in template tags (#1056) 2025-07-29 12:18:37 -06:00
rmcrackan 7741e3caff incr ver 2025-07-29 07:54:24 -04:00
rmcrackan c82eefa768 Merge pull request #1317 from Mbucari/master
Bug fixes and UI tweak
2025-07-29 07:52:01 -04:00
Michael Bucari-Tovo 0e4231906a Set IsSpatial field instead of only flipping to true (#1273) 2025-07-28 15:28:04 -06:00
Michael Bucari-Tovo 9bca84dca4 Sort columns with null values always at the bottom 2025-07-28 09:29:17 -06:00
MBucari ca30fd41c6 Use proper version string based on build version 2025-07-28 08:59:20 -06:00
MBucari be96f99461 Increment Version 2025-07-27 11:40:35 -06:00
MBucari f017fe419f Fix ID3 tag encoding error (#1315) 2025-07-27 11:38:01 -06:00
rmcrackan ed42916cb2 incr ver 2025-07-26 23:11:51 -04:00
rmcrackan 0bb5bba3c8 Merge pull request #1314 from Mbucari/master
New audio format features, bug fixes, and minor tweaks/improvements.
2025-07-26 23:10:05 -04:00
MBucari a887bf4619 Add "Is Spatial" grid column. 2025-07-26 18:19:19 -06:00
MBucari 53eebcd6ba Use single file downloader/namer if file has only 1 chapter 2025-07-25 16:02:28 -06:00
MBucari a09ae1316d Don't display null file versions 2025-07-25 16:01:48 -06:00
MBucari 7088bd4b8d Check for file existance 2025-07-25 15:49:41 -06:00
MBucari b27325cdcb Improve comvert to mp3 task
- Improve progress reporting and cancellation performance
- Clear current book from queue before queueing single convert to mp3 task
2025-07-25 15:35:03 -06:00
MBucari accedeb1b1 Improve EditQuickFilters dialog reordering behavior 2025-07-25 14:23:14 -06:00
MBucari c98c7c095a Fix quickfilter modification bug (#1313) 2025-07-25 14:22:29 -06:00
MBucari 9b217a4e18 Add audio format data
- Add Book.IsSpatial property and add it to search index
- Read audio format of actual output files and store it in UserDefinedItem. Now works with MP3s.
- Store last downloaded audio file version
- Add IsSpatial, file version, and Audio Format to library exports and to template tags. Updated docs.
- Add last downloaded audio file version and format info to the Last Downloaded tab
- Migrated the DB
- Update AAXClean with some bug fixes
  - Fixed error converting xHE-AAC audio files to mp3 when splitting by chapter (or trimming the audible branding from the beginning of the file)
  - Improve mp3 ID# tags support. Chapter titles are now preserved.
  - Add support for reading EC-3 and AC-4 audio format metadata
2025-07-25 12:18:50 -06:00
Michael Bucari-Tovo a62a9ffc5b Use HttpClient in synchronous mode 2025-07-23 17:00:54 -06:00
Michael Bucari-Tovo 08aebf8ecf Add thread safety 2025-07-23 17:00:36 -06:00
Michael Bucari-Tovo 2f082a9656 Refactor and optimize audiobook download and decrypt process
- Add more null safety
- Fix possible FilePathCache race condition
- Add MoveFilesToBooksDir progress reporting
- All metadata is now downloaded in parallel with other post-success tasks.
- Improve download resuming and file cleanup reliability
- The downloader creates temp files with a UUID filename and does not insert them into the FilePathCache. Created files only receive their final file names when they are moved into the Books folder. This is to prepare for a future plan re naming templates
2025-07-23 16:55:09 -06:00
Michael Bucari-Tovo 1f473039e1 Make search syntax dialog field names scrollable 2025-07-22 15:39:43 -06:00
Michael Bucari-Tovo 0f4197924e Use LibationUiBase.ReactiveObject where applicable
Also tweak the classic process queue control layout
2025-07-22 11:59:34 -06:00
rmcrackan 0f7ffacdf8 incr ver 2025-07-22 10:20:39 -04:00
rmcrackan 829b35c5a8 Merge pull request #1311 from Mbucari/master
Fix serilog dynamic assembly loading issue (#1310)
2025-07-22 10:18:33 -04:00
Michael Bucari-Tovo 614b05d5ff Fix serilog dynamic assembly loading issue (#1310) 2025-07-22 08:00:31 -06:00
rmcrackan 26ccc77b47 incr ver 2025-07-22 07:24:26 -04:00
rmcrackan 64fb2ccf7c Merge pull request #1308 from Mbucari/master
Refactors, bug fixes, and performance improvements.
2025-07-22 07:22:35 -04:00
MBucari 890747a902 Do library scan on background thread 2025-07-22 00:20:16 -06:00
Michael Bucari-Tovo 1fdcea929f Form thread safety 2025-07-21 22:52:17 -06:00
Michael Bucari-Tovo 7848366818 Write logs to text .log file instead of .zip file
The ZipFile sink could cause program hangs. Additionally, the only reason it was ever used was to package verbose AudibleApi account login errors, saving the returned Html page as a file. Otherwise, the zip file only contains a .log text file.

- Removed Serilog.Sinks.ZipFile
- Add Serilog configuration migration
- Added a custom destructure to handle logging files. If any files are logged, they will be written to "LogyyyyMM_AdditionalFiles.zip"
2025-07-21 22:19:55 -06:00
Michael Bucari-Tovo 40b4915b65 Improve download/decrypt cancellation 2025-07-21 15:56:41 -06:00
Michael Bucari-Tovo 80b86086ca Consolidate process queue view models
Remove classic and chardonnay-specific implementations
Refactor TrackedQueue into an IList with INotifyCollectionChanged
2025-07-21 15:56:30 -06:00
Michael Bucari-Tovo bff9b67b72 Remove GridEntry derrived types and interfaces
Use existing BaseUtil.LoadImage delegate, obviating need for derrived classes to load images

Since GridEntry types are no longer generic, interfaces are unnecessary and deleted.
2025-07-21 10:47:10 -06:00
Mbucari 657a7bb6bc Improve podcast episode GridEntry creation performance.
Tested on a library with ~5000 podcast episodes on an AMD Ryzen 7700X. Startup time decreases by ~400 ms in Release mode.
2025-07-21 09:49:25 -06:00
rmcrackan f0d7a7bf64 incr ver 2025-07-18 07:19:09 -04:00
rmcrackan 8bc098e7bd Merge pull request #1303 from Mbucari/master
Fix upgrade bug when Libation's working dir isn't program files dir
2025-07-18 07:16:46 -04:00
Michael Bucari-Tovo 9280b29512 Fix upgrade bug when Libation's working dir isn't program files dir
Add MockUpgrader for testing the Upgrade process.
Fixes issue #1302
2025-07-17 13:10:42 -06:00
rmcrackan d8e9b9c505 incr ver 2025-07-17 08:07:08 -04:00
rmcrackan 554b308364 Merge pull request #1299 from Mbucari/master
Bugfixes and minor improvements
2025-07-17 08:04:43 -04:00
MBucari 8d7872a376 UI tweak and optimization 2025-07-16 23:31:34 -06:00
MBucari 747451d243 Refactor Classic process queue
The queue is now more MVVM-like.
2025-07-16 22:58:03 -06:00
MBucari 7e79e98771 Fix possible cross-threading errors with MessageBoxBase 2025-07-16 22:57:25 -06:00
Michael Bucari-Tovo 4b7939541a Code cleanup and refactoring for clarity 2025-07-16 22:55:57 -06:00
MBucari a3734c76b1 Use SynchronizeInvoker's Invoke() method. 2025-07-15 23:22:42 -06:00
MBucari ced4ea6c17 Improve sorting by Liberate status by grouping books with PDFs 2025-07-15 22:50:53 -06:00
MBucari 35ca6f2621 Use built-in comparer and ReactiveObject types 2025-07-15 22:50:28 -06:00
MBucari 4dab16837e Move ProcessQueueViewModel logic into LibationUiBase
Fix UI bug in classic when queue is in popped-out mode.
2025-07-15 22:31:17 -06:00
MBucari 1cf889eed7 Move ProcessBookViewModel logic into LiationUiBase 2025-07-15 15:05:33 -06:00
MBucari b65b1e819b Consolidate queue commands into UI base 2025-07-15 13:32:42 -06:00
MBucari 3d50643ab0 Fix visible book counts being incorrect on startup
If quick filters are applied on startup, a race condition was created between the initial library load book counting and the visible books counting. Only display results of the latest book count.
2025-07-15 11:49:20 -06:00
MBucari abd18d74b0 Fix crash when setting drive root as custom directory (#1300) 2025-07-15 11:44:45 -06:00
MBucari 0e49df06b8 Add message box handler to LibationUiBase 2025-07-15 11:40:01 -06:00
MBucari 38cc3e9725 Revert change to release title 2025-07-15 08:54:22 -06:00
MBucari c9af2bba4b Reduce GitHub API calls when no upgrades are available 2025-07-14 14:43:48 -06:00
MBucari 2191c1536d Prepare Libation for win-arm64 releases
Also add support for four-part version numbers in releases.
2025-07-14 14:20:57 -06:00
MBucari 5b9bf2fbb0 Remove duplicate tests 2025-07-14 12:53:47 -06:00
MBucari 9b1ce8c1d7 Update dependencies 2025-07-14 12:43:53 -06:00
MBucari 9f8075041b Only remove a LibraryBook from queue if we are trying to re-download. 2025-07-14 12:42:05 -06:00
MBucari 944645379e Fix message box text truncation when there is no icon (#1294) 2025-07-14 12:19:26 -06:00
Mbucari cc72517284 Merge branch 'rmcrackan:master' into master 2025-07-14 11:45:44 -06:00
rmcrackan 0044820415 Update README.md 2025-07-07 16:31:09 -04:00
rmcrackan 9f24027de1 Update README.md 2025-07-07 16:29:46 -04:00
rmcrackan 24f95cb03d Update GettingStarted.md 2025-07-07 16:27:59 -04:00
rmcrackan 3aeea54615 Update FrequentlyAskedQuestions.md 2025-07-07 16:26:10 -04:00
rmcrackan f511041781 Create a cue sheet: default false 2025-06-25 12:43:50 -04:00
rmcrackan da9dc91469 incr ver for docker enhancement 2025-06-25 06:58:14 -04:00
rmcrackan e04e70d333 Merge pull request #1265 from vipervire/master
Update Books directory to use LIBATION_BOOKS_DIR if populated
2025-06-25 06:57:01 -04:00
rmcrackan e0b566ee60 Merge pull request #1277 from dev-nicolaos/patch-1
Update deb/rpm Installation Instructions
2025-06-24 07:50:35 -04:00
Nicolaos Skimas bf15d7302e Update Deb/RHEL/Fed Installation Instructions 2025-06-23 22:39:45 -07:00
rmcrackan 8f01c644c0 Update bug_report.md 2025-06-19 07:21:21 -04:00
Mbucari ebd2cc96c5 Merge branch 'rmcrackan:master' into master 2025-06-18 12:13:14 -06:00
rmcrackan 0d1cc42ca7 Bugfix #1269 : Chardonnay. Bad filter string causes infinite loop 2025-06-16 13:19:48 -04:00
vipervire e126dd09ce Update Books directory to use LIBATION_BOOKS_DIR if populated 2025-06-05 23:26:52 +00:00
Michael Bucari-Tovo ec497f4f81 Use virtualized list to improve large queue performance 2025-05-19 10:40:41 -06:00
rmcrackan 248fdfd2bc Probably unnecessary paranoid incr ver. Everything looks correct but I've never actually released relying on the ver's 4th part. I'm incrementing just in case 2025-05-10 16:53:04 -04:00
MBucari 35862d619a Increment version 2025-05-09 21:10:38 -06:00
Mbucari ac2c67985d Merge pull request #1253 from Mbucari/master
Fix download error (#1252 )

I'm merging this one and releasing ASAP.
2025-05-09 21:07:59 -06:00
MBucari f8ae303417 Fix download error (#1252 ) 2025-05-09 21:07:01 -06:00
rmcrackan 0d24caeac2 incr ver 2025-05-09 21:10:19 -04:00
rmcrackan 7f1b357c52 Merge pull request #1250 from Mbucari/master
Bug fixes and a change to license request logic
2025-05-09 21:08:19 -04:00
Michael Bucari-Tovo ef67ae9d6a Ask users to clear the accounts when enabling widevine (#1249) 2025-05-09 17:52:14 -06:00
Michael Bucari-Tovo f35c82d59d Change ApiExtended to always allow provide login option
Previously, only some calls to ApiExtended.CreateAsync() would prompt users to login if necessary. Other calls would only work if the account already had a valid identity, and they would throw exceptions otherwise.

Changed ApiExtended so that the UI registers a static ILoginChoiceEager factory delegate that ApiExtended will use in the event that a login is required.
2025-05-09 17:32:12 -06:00
Michael Bucari-Tovo 10c01f4147 Fix occasional error of audio downloads hanging. 2025-05-09 16:32:59 -06:00
Michael Bucari-Tovo 9366b3baca Default to E-AC-3 spatial audio format. 2025-05-09 13:39:59 -06:00
Michael Bucari-Tovo 20e792c589 Always change the last chapter's length to coincide with the end of the audio file. 2025-05-09 13:36:07 -06:00
Michael Bucari-Tovo dfb63d3275 Add contributor 2025-05-09 13:15:18 -06:00
Michael Bucari-Tovo 19db226f5a Use Libation settings to decide which DRM is downloaded. 2025-05-09 13:13:39 -06:00
Mbucari 203ab00865 Merge branch 'rmcrackan:master' into master 2025-05-08 12:15:26 -06:00
MBucari b11a4887d7 Pad final chapter to prevent tuncation from incorrect chapter info (#1246) 2025-05-08 12:13:55 -06:00
rmcrackan e73fc5e1eb Merge pull request #1247 from starry-shivam/patch-2
Small typo fix in DownloadOptions.Factory.cs
2025-05-08 07:12:42 -04:00
Stɑrry Shivɑm 8561a15061 Small typo fix in DownloadOptions.Factory.cs 2025-05-08 10:48:02 +05:30
MBucari 28ba62aead Fix dash files not being saved (#1236) 2025-05-07 23:15:44 -06:00
rmcrackan 176294cc55 Merge pull request #1245 from Mbucari/master
Minor bugfixes
2025-05-07 19:28:45 -04:00
Michael Bucari-Tovo 152b0e362d Update message box icons 2025-05-07 16:10:03 -06:00
Michael Bucari-Tovo 4600d029dc Re-add converter resource inadvertantly removed in 0df17a22 2025-05-07 14:23:58 -06:00
Michael Bucari-Tovo 1a5684799c Update Hangover styles and behaviors 2025-05-07 13:16:44 -06:00
Michael Bucari-Tovo 0df17a2296 Remove retired ItemsRepeater control 2025-05-07 13:12:12 -06:00
Michael Bucari-Tovo 45472abd1f Update dependencies 2025-05-07 11:15:32 -06:00
Mbucari f2ea4539f2 Merge branch 'rmcrackan:master' into master 2025-05-07 11:13:32 -06:00
Michael Bucari-Tovo 52d3b9cb67 Disable warning 2025-05-07 11:13:26 -06:00
rmcrackan 3d87f2cd9b Merge branch 'master' of https://github.com/rmcrackan/Libation 2025-05-07 12:39:10 -04:00
rmcrackan e4a3d2ac79 better logging for api errors #1240 2025-05-07 12:39:02 -04:00
Michael Bucari-Tovo 8aa157f2f6 Re-add completed audiobooks to queue (#1219) 2025-05-06 15:43:58 -06:00
Michael Bucari-Tovo 5ab6c1fe70 Update AAXClean to fix metadata reader (#1243 ) 2025-05-06 15:33:38 -06:00
Michael Bucari-Tovo b23c46f79f Fix incorrect chapters in some audiobooks (#1210) 2025-05-06 15:32:59 -06:00
Mbucari 0e987eef00 Fix error in download speed throttle (#1242) 2025-05-06 14:48:40 -06:00
rmcrackan ace3d80e41 Merge pull request #1241 from cherez/patch-1
Fixed doubled first name in templates
2025-05-06 16:29:29 -04:00
Mbucari 4bfb4e73ce Fix aax file getting inadvertently deleted (#1236) 2025-05-06 12:45:43 -06:00
Steven Wallace 7805a3ef11 Fixed broken single word name test
This expected the name duplication that the previous commit fixed to be the behavior, changed to expect the single word to be the last name.
2025-05-06 09:58:09 -05:00
Steven Wallace 08ca2a2db3 Fixed doubled first name in templates
v12.3.0 caused a regression with contributors with a single word name, causing the name to be doubled. This was caused by using that name as both the first and last name, so swap the first name with the (blank) last name rather than duplicate them.
2025-05-05 10:37:28 -05:00
rmcrackan 64a85b6aab Merge branch 'master' of https://github.com/rmcrackan/Libation 2025-05-02 22:15:36 -04:00
rmcrackan 1a38273d5f incr ver 2025-05-02 22:15:32 -04:00
rmcrackan 303dd7c471 Merge pull request #1233 from Mbucari/master
Bugfixes and Feature Requests
2025-05-02 22:14:33 -04:00
MBucari 313e3846c3 Remove AudioFormat from library book exporter (5f455182) 2025-05-02 15:39:47 -06:00
Michael Bucari-Tovo 422c86345e Add logging 2025-05-02 14:50:33 -06:00
Michael Bucari-Tovo ce952417fb Don't replace library properties in queued item with null/empty 2025-05-02 13:07:53 -06:00
Michael Bucari-Tovo 5f4551822b Remove Book.AudioFormat property
This property was set to the highest quality returned by the library scan. Since adding quality option settings, it is no longer guaranteed to reflect the file that is downloaded. Also, the library scan qualities don't contain spatial audio or widevine-specific qualities., only ADRM.
2025-05-02 12:39:12 -06:00
Michael Bucari-Tovo 3aebc7c885 Improve download performance. 2025-05-02 12:19:32 -06:00
Michael Bucari-Tovo 3982edd0f1 Add codec tag and use real bitrate/samplerate (#1227) 2025-05-02 11:20:58 -06:00
Michael Bucari-Tovo f4dafac28f Try to solve #1226 2025-05-01 13:19:03 -06:00
Michael Bucari-Tovo 1090d29f74 Add fine-grained options for downloading widevine content 2025-05-01 13:03:03 -06:00
rmcrackan 1c336e1fe9 bug fix 2025-04-28 18:55:20 -04:00
rmcrackan c7e9e9ac1e incr ver 2025-04-28 13:36:26 -04:00
rmcrackan 8232b2b5e5 Merge pull request #1223 from Mbucari/master
New features, including spatial audio support
2025-04-28 13:34:40 -04:00
MBucari 9ca879cc3d Revert "Allow re-adding completed queued items"
This reverts commit e2aae85fd7.
2025-04-27 14:31:21 -06:00
MBucari ece48eb6d7 Add spatial audio support 2025-04-27 14:31:14 -06:00
MBucari bffaea6026 Add CDM API url list 2025-04-27 13:15:50 -06:00
MBucari e2aae85fd7 Allow re-adding completed queued items 2025-04-25 19:54:19 -06:00
MBucari 1777dc5a7e Update AAXClean.Codecs and dependencies 2025-04-25 19:52:51 -06:00
Mbucari 2dfe00f428 Merge branch 'rmcrackan:master' into master 2025-04-15 00:36:11 -06:00
rmcrackan 2cd0a022ff bug fix #1212 : fix window title 2025-04-03 08:20:31 -04:00
Michael Bucari-Tovo 5d7ac699e6 Mark unreleased books as unavailable (#1079) 2025-03-25 12:35:18 -06:00
Michael Bucari-Tovo 7d806e0f3e Increase tag template options for contributor and series types
- Add template tag support for multiple series
- Add series ID and contributor ID to template tags
- <first author> and <first narrator> are now name types with name formatter support
- Properly import contributor IDs into database
- Updated docs
2025-03-25 09:34:57 -06:00
Michael Bucari-Tovo 0a9e489f48 Move contributors to UI Base 2025-03-24 13:29:02 -06:00
rmcrackan 17612dacd2 incr ver 2025-03-22 06:35:30 -04:00
rmcrackan e61ad41d5a Merge pull request #1202 from Mbucari/master
Add multiselect feature and a bugfix
2025-03-22 06:31:33 -04:00
Michael Bucari-Tovo c77f2e2162 Add multi-select context menu support (rmcrackan/Libation#1195) 2025-03-21 16:49:21 -06:00
Michael Bucari-Tovo bfcd226795 Fix libation hanging on first inport of large libraries 2025-03-21 11:08:36 -06:00
rmcrackan 0af7c4d90a fix ver 2025-03-21 09:19:03 -04:00
rmcrackan e4826388be incr ver 2025-03-21 09:18:48 -04:00
rmcrackan 98a1fa4dda Merge pull request #1193 from Mbucari/master
Add support for custom themes in chardonnay
2025-03-21 09:12:35 -04:00
Michael Bucari-Tovo 81e9ab7fb2 Fix theme not resetting properly
Change button foreground color
2025-03-20 16:30:08 -06:00
Mbucari 9c82d34ba4 Merge branch 'rmcrackan:master' into master 2025-03-20 15:30:18 -06:00
Mbucari a384bceab0 Update Readme for Chardonnay Themes 2025-03-20 15:29:46 -06:00
Michael Bucari-Tovo 545540d9a4 Improve Libation glass icons for use with dark mode. 2025-03-20 15:04:22 -06:00
MBucari f402912a92 Mark resource as dynamic and delete unused resource 2025-03-19 22:43:50 -06:00
Michael Bucari-Tovo aab4f1d9d6 Add theme import and export function 2025-03-19 21:47:24 -06:00
Michael Bucari-Tovo f183b587b8 Revert all changes if window is closed by user. 2025-03-19 16:38:58 -06:00
Michael Bucari-Tovo 733a091ebd Add theme preview dialog 2025-03-19 16:26:14 -06:00
Mbucari 9043ea6334 Merge branch 'rmcrackan:master' into master 2025-03-19 14:21:51 -06:00
Michael Bucari-Tovo 40890f242a Fix spelling errors 2025-03-19 14:16:32 -06:00
rmcrackan 6c03f525bf Update InstallOnMac.md -- minimum OS supported 2025-03-19 16:01:40 -04:00
Michael Bucari-Tovo dcda1a0cc2 Add contributors to about page 2025-03-18 21:18:25 -06:00
Michael Bucari-Tovo e509f842e4 Remove unused windows forms buttons and streamline dialogs 2025-03-18 21:18:25 -06:00
Mbucari faa2e04b9f Merge branch 'rmcrackan:master' into master 2025-03-18 21:17:30 -06:00
Robert McRackan 71afb5c9f4 incr ver 2025-03-18 21:09:27 -04:00
Michael Bucari-Tovo d90ef3f4d4 Mark IconFill as a dynamic resource 2025-03-18 12:33:01 -06:00
Michael Bucari-Tovo f84bb753e9 Revert custom window border on Windows 2025-03-13 16:44:16 -06:00
Michael Bucari-Tovo b34970bd47 Add support for custom themes in chardonnay 2025-03-13 16:05:32 -06:00
Robert McRackan a37eb383cd Merge branch 'master' of https://github.com/rmcrackan/Libation 2025-03-10 19:11:22 -04:00
Robert McRackan 614965e1ab incr ver 2025-03-10 19:09:44 -04:00
rmcrackan 52d611a74c Merge pull request #1189 from Mbucari/master
Minor bug fixes.
2025-03-10 17:30:24 -04:00
Michael Bucari-Tovo 653381b1df Fix Auto download not working sometimes (#1183) 2025-03-10 12:57:52 -06:00
Michael Bucari-Tovo 4e067f5b5b Remove inadvertently committed debugging code 2025-03-10 10:46:36 -06:00
Michael Bucari-Tovo ee05ca4eb2 Handle corrupted LibraryScans.zip file (#1185) 2025-03-10 09:49:22 -06:00
Michael Bucari-Tovo 65e12d9a8f Add default window dimensions 2025-03-10 09:07:35 -06:00
rmcrackan 5dc1bafb94 Update Docker.md 2025-03-07 10:12:05 -05:00
rmcrackan 3010f80834 Merge pull request #1181 from rmcrackan/dependabot/nuget/Source/LibationUiBase/SixLabors.ImageSharp-3.1.7
Bump SixLabors.ImageSharp from 3.1.6 to 3.1.7 in /Source/LibationUiBase
2025-03-06 20:49:34 -05:00
dependabot[bot] 6c20b85200 Bump SixLabors.ImageSharp from 3.1.6 to 3.1.7 in /Source/LibationUiBase
Bumps [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/SixLabors/ImageSharp/releases)
- [Commits](https://github.com/SixLabors/ImageSharp/compare/v3.1.6...v3.1.7)

---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 22:29:43 +00:00
rmcrackan bf87180fe9 Merge pull request #1179 from Mbucari/master
UI tweak and Linux command updates
2025-03-05 17:41:47 -05:00
Michael Bucari-Tovo ae9aac789f Use polkit (#1176) and dnf5/dnf (#1177) 2025-03-05 14:22:51 -07:00
Mbucari e6cd182872 Merge branch 'rmcrackan:master' into master 2025-03-05 11:25:50 -07:00
Michael Bucari-Tovo 7eeb2dcd7f Move encoding options above to the mp3 settings column (#764)
Additionally, add more tool tips for cryptic options.
2025-03-05 11:17:47 -07:00
rmcrackan 71bb0571d1 Merge pull request #1178 from RokeJulianLockhart/patch-1
Add instructions for Fedora to `InstallOnLinux.md`.
2025-03-05 11:32:10 -05:00
Mr. Beedell, Roke Julian Lockhart 7bb5b2968e Add instructions for Fedora to InstallOnLinux.md
Partially improves upon https://github.com/rmcrackan/Libation/issues/1177#issue-2897597653.
2025-03-05 15:17:49 +00:00
rmcrackan b051283fca Update bug_report.md 2025-03-05 09:30:43 -05:00
rmcrackan 53af2ee39e Update feature_request.md 2025-03-05 09:30:18 -05:00
rmcrackan fab32a1744 Update bug_report.md 2025-03-05 09:29:04 -05:00
rmcrackan e2dabc8a53 Update feature_request.md 2025-03-05 09:26:47 -05:00
rmcrackan fd82f7ae5a Update feature_request.md 2025-03-05 09:26:19 -05:00
rmcrackan df9535a83d Update FrequentlyAskedQuestions.md 2025-03-05 08:20:42 -05:00
rmcrackan 85b6792468 Merge pull request #1175 from Mbucari/master
Additional null safety
2025-03-04 21:32:35 -05:00
Michael Bucari-Tovo e37abbf276 Fix dark theme text color in DataGridTextColumn 2025-03-04 16:18:06 -07:00
Michael Bucari-Tovo c3938c49a9 Additional null safety 2025-03-04 15:41:26 -07:00
Michael Bucari-Tovo 7658f21d7c Fix tags font color in dark mode 2025-03-04 15:07:37 -07:00
Michael Bucari-Tovo c4827fc761 Add error logging 2025-03-04 12:54:05 -07:00
rmcrackan 649b52af1d Merge pull request #1174 from Mbucari/master
Null safety & UI tweak
2025-03-04 13:10:51 -05:00
Michael Bucari-Tovo da06511951 Only show buttons on mouse over 2025-03-04 10:34:09 -07:00
Michael Bucari-Tovo 88d3e5ff0c Null safety checks. 2025-03-04 10:33:29 -07:00
rmcrackan 5f99e594d8 Merge pull request #1172 from Mbucari/master
Thread safety and AccountSettings.json error handling
2025-03-03 19:27:45 -05:00
Michael Bucari-Tovo 981a183992 Add a border around dialogs with CanResize=true 2025-03-03 15:20:58 -07:00
Michael Bucari-Tovo ac036f65f1 Handle and notify users of invalid account settings file 2025-03-03 14:41:56 -07:00
Michael Bucari-Tovo b36e110b49 Add thread safety and comments re threading 2025-03-03 10:11:17 -07:00
Robert McRackan ef3c71a939 Incr ver 2025-03-02 20:34:45 -05:00
rmcrackan b2af93bed9 Merge pull request #1169 from Mbucari/master
12.0.1 Bug Fixes
2025-03-02 20:33:59 -05:00
Mbucari 1f427919e6 Try to solve threadding issues (#1168) 2025-03-02 11:59:35 -07:00
Mbucari c9c5bbb687 Fix errorre rmoving entries from the cache (#1167) 2025-03-02 10:55:26 -07:00
Robert McRackan efbefa2784 Increm ver 2025-03-01 10:44:49 -05:00
rmcrackan 51aabe5dd4 Merge pull request #1161 from Mbucari/master
Performance and UI tweaks
2025-02-28 22:48:49 -05:00
Michael Bucari-Tovo 1c668adff8 Deliminated category names in library exports with semicolon (#1107) 2025-02-28 17:35:58 -07:00
Michael Bucari-Tovo 4170dcc1d5 Chardonnay UI bug fixes and improvements
- Theme changes do not require restart
- Fix some text appearing black in dark mode
- Fix dialog boxes not appearing correctly on Windows
- Fix queue vertical scroll bar overlapping items
2025-02-28 17:34:55 -07:00
Michael Bucari-Tovo 68cfae1d58 Fix ever-widening Form1 when form size is restored. 2025-02-28 12:04:49 -07:00
Michael Bucari-Tovo a790c7535c Changes to default directories for file storage (#1112)
- Add My Music and Local Application Data to known directories
- Make %localappdata%\Libation the default settings folder on *nix machines
- Make %MyMusic%\Libation\Books the default books folder on *nix machines
2025-02-28 12:01:58 -07:00
Michael Bucari-Tovo 3b7d5a354f Re-add books to queue that failed or were cancelled. 2025-02-28 10:47:26 -07:00
MBucari a9375f1520 Improve file cache performance and add migration
LibraryCommands.GetCounts hits the file cache hard. The previous cache implementation was linear list, so finding an entry by ID was (n). When you consider that each book may have many files, the number of cache entries could grow to many multiples of the library size.

The new cache uses a dictionary with the ID as its key, and a CacheEntry list as its value.
2025-02-28 10:07:45 -07:00
Michael Bucari-Tovo 47c9fcb883 Improve LibrarySizeChanged performance 2025-02-27 22:56:30 -07:00
rmcrackan 5f5c9f65ed Merge pull request #1160 from Mbucari/master
Fixed stack overflow crash when movifying large databases
2025-02-27 21:41:33 -05:00
MBucari 1417a4b992 Improve library load performance 2025-02-27 19:16:36 -07:00
Michael Bucari-Tovo 2d6120f0c4 Get full library in LibrarySizeChanged event and pass as EventArgs
There are multiple subscribers to LibraryCommands.LibrarySizeChanged, and each one calls GetLibrary_Flat_NoTracking(). Passing the full library as an event argument speeds up all operations which happen after the library size changes.

Fix initial backup counts
2025-02-27 13:11:28 -07:00
Michael Bucari-Tovo 2a25b7e0ad Load MainWindow before library finishes loading like Classic 2025-02-27 11:17:18 -07:00
Michael Bucari-Tovo 4766ea7372 Improve NRE safety for quick filters 2025-02-27 10:10:26 -07:00
Michael Bucari-Tovo d195dd07dc Remove upload-release-assets package (out of support) 2025-02-27 09:30:08 -07:00
Michael Bucari-Tovo bcbb7610ad Refactor GetAllEntries methods for clarity and maintainability
d
2025-02-27 09:00:26 -07:00
Michael Bucari-Tovo 6c5773df24 Fix stack overflow exception when updating large databases (#1158) 2025-02-26 14:41:59 -07:00
rmcrackan 211f15af25 Update InstallOnMac.md 2025-02-25 10:03:11 -05:00
Robert McRackan e3b0f80016 incr ver: 12.0 2025-02-24 12:05:24 -05:00
Robert McRackan 7b2c7e49e5 fix unit tests 2025-02-23 11:11:25 -05:00
Robert McRackan 6a40d19393 Fix unit tests 2025-02-23 10:28:09 -05:00
Robert McRackan 3167744111 #1151 : default character replacement for colon is _ 2025-02-23 10:13:15 -05:00
Robert McRackan b6a3ba335a Merge branch 'master' of https://github.com/rmcrackan/Libation 2025-02-22 22:54:12 -05:00
Robert McRackan fa1ddc726a #1151 - unicode colon is causing issues with Windows Chardonnay 2025-02-22 22:54:00 -05:00
rmcrackan b3b0662dec Update InstallOnMac.md
Apple can’t check app for malicious software
2025-02-19 17:09:26 -05:00
Robert McRackan 5cb22cfd24 Update AAXClean.Codecs 2025-02-17 20:07:50 -05:00
Michael Bucari-Tovo e911344850 Workaround for DataGridView filtering internal NullReferenceException 2025-02-10 10:20:18 -07:00
Michael Bucari-Tovo 8ec7e5a9d2 Revert "DataGridView filtering internal NullReferenceException **HACK**"
This reverts commit e1f749c3da.
2025-02-10 10:19:24 -07:00
Robert McRackan e1f749c3da DataGridView filtering internal NullReferenceException **HACK** 2025-02-10 11:23:27 -05:00
Robert McRackan ba060d15aa Merge branch 'master' of https://github.com/rmcrackan/Libation 2025-02-10 09:12:32 -05:00
Robert McRackan 93fde236c8 format for clarity 2025-02-10 09:12:29 -05:00
Michael Bucari-Tovo 13aad1a7cb Restrict audio sample rate settings to allowed values (#1116) 2025-01-16 10:35:58 -07:00
rmcrackan 65c64c4504 Update Docker.md 2025-01-13 09:28:55 -05:00
rmcrackan 14ba04c28b Update Docker.md 2025-01-13 09:26:52 -05:00
rmcrackan 96e886d207 Update FrequentlyAskedQuestions.md
Brazil login
2025-01-08 10:04:55 -05:00
Robert McRackan c7279574a9 Upgrade avalonia 2025-01-07 08:00:35 -05:00
rmcrackan a522e1ff7e Merge pull request #1072 from shuvashish76/patch-1
Update InstallOnLinux.md with repology badge
2024-12-08 08:56:32 -05:00
shuvashish76 9ebc4444bd Update InstallOnLinux.md 2024-12-07 06:24:34 +00:00
shuvashish76 525afdf050 Update InstallOnLinux.md 2024-12-07 06:03:46 +00:00
shuvashish76 983cdf6ad5 Update InstallOnLinux.md with repology badge 2024-12-07 05:38:35 +00:00
Robert McRackan 09bb32a435 incr ver 2024-12-06 13:10:37 -05:00
rmcrackan 817ef33fbd Merge pull request #1071 from pixil98/master
update docker image to dotnet 9
2024-12-06 13:07:39 -05:00
Aaron Reisman be52b496a6 Update docker image to .net 9 2024-12-06 10:29:47 -06:00
Robert McRackan c0c99db6fa Incr ver 2024-12-05 14:54:16 -05:00
rmcrackan a1c8fb5921 Merge pull request #1066 from Mbucari/master
Fix chardonnay window closing (#1065)
2024-12-05 14:53:11 -05:00
MBucari 4576c0e193 Updata Avalonia to 11.2.2 2024-12-04 19:12:59 -07:00
MBucari d592e9435e Fix main window closing when dialog is closed (#1065) 2024-12-04 19:11:36 -07:00
Robert McRackan 9ce6cb54ab incr ver 2024-11-22 17:18:39 -05:00
Robert McRackan c15d49fc64 publish profiles => .net 2024-11-22 17:04:08 -05:00
Robert McRackan 99be869aa9 yaml => .net9 2024-11-22 16:56:16 -05:00
Robert McRackan a0e875a79c Merge branch 'master' of https://github.com/rmcrackan/Libation 2024-11-22 16:49:09 -05:00
Robert McRackan 6134becc70 Upgrade to .net9 2024-11-22 16:47:59 -05:00
rmcrackan eadf7cff79 Merge pull request #1050 from stickystyle/patch-1
set maxdepth to prevent traversal into subdirectories
2024-11-20 13:13:58 -05:00
Ryan Parrish 87ca76f9cb set maxdepth to prevent traversal into subdirectories 2024-11-20 11:32:02 -05:00
Robert McRackan 43d1019059 * Bug fix #1048: docker: Error when using SLEEP_TIME - "integer expression expected"
Thanks @charltonstanley !
2024-11-19 06:40:51 -05:00
Robert McRackan ed87ded77a Docker deployment fix 2024-11-14 22:51:02 -05:00
rmcrackan 56d4205360 Merge pull request #1042 from pixil98/master
Exclude docker build report artifact from release packing
2024-11-14 22:50:26 -05:00
Aaron Reisman 1f839606ae Add a pattern to exclude docker build artifact from release 2024-11-14 16:33:30 -06:00
Robert McRackan 6eebe652d4 Docker changes => pre-release 2024-11-14 15:22:28 -05:00
rmcrackan 5fff22a0e1 Merge pull request #1011 from pixil98/master
Run docker image as non-root user
2024-11-14 15:19:36 -05:00
Aaron Reisman cd7040cdc7 pretty up the workflows 2024-11-14 11:18:16 -06:00
Aaron Reisman 97b792868f Update documentation with new envvars 2024-11-14 10:54:35 -06:00
pixil98 984f931f67 Merge branch 'rmcrackan:master' into master 2024-11-14 01:04:16 -06:00
Aaron Reisman e0dd9b845a rework database handling 2024-11-14 00:59:28 -06:00
rmcrackan f1c8b320c2 Merge pull request #1039 from jwillikers/fix-metadata
Fix metainfo validation
2024-11-13 21:09:45 -05:00
Jordan Williams 9b7d0cd909 Fix metainfo validation 2024-11-13 15:14:14 -06:00
rmcrackan 99592ff84e Merge pull request #1036 from jwillikers/flatpak-files
Add some necessary files for the Flatpak
2024-11-13 15:36:18 -05:00
Jordan Williams f97cfe77f9 Add CI to validate the desktop file and appstream metainfo 2024-11-10 15:09:56 -06:00
Jordan Williams 2954cb961b Add metainfo and screenshots 2024-11-10 14:05:24 -06:00
Jordan Williams 1e29b98b82 Add categories and keywords to the desktop file 2024-11-10 14:05:24 -06:00
Robert McRackan 8b76da0dbe incr ver 2024-10-25 15:05:08 -04:00
Mbucari 0a749d2d88 Update Bundle_MacOS.sh 2024-10-25 10:51:56 -06:00
Aaron Reisman 9ed6c1fd0d cleanup 2024-10-22 10:07:37 -05:00
Aaron Reisman 9825e2b552 Build both platforms in one action 2024-10-22 09:27:00 -05:00
Aaron Reisman 011efe3676 remove unused configure step 2024-10-22 00:39:35 -05:00
Aaron Reisman 2bdcc221f5 Specify platform(?) 2024-10-22 00:28:27 -05:00
pixil98 21bedca367 Merge branch 'rmcrackan:master' into master 2024-10-21 23:55:31 -05:00
Aaron Reisman 074fe79ded Update docker workflow to try building on validate 2024-10-21 17:13:08 -05:00
Aaron Reisman ac8c090c4c Rework run script to support db mount better 2024-10-21 14:02:52 -05:00
Aaron Reisman ade693bebb Update docker readme 2024-10-19 01:54:37 -05:00
Aaron Reisman 9bc53e45cd large overhaul of docker run script 2024-10-19 01:31:03 -05:00
Aaron Reisman 7d4eaa11e7 Run docker image as non-root user 2024-10-18 00:13:19 -05:00
Robert McRackan 4521c5d5ed incr ver 2024-10-16 12:04:04 -04:00
rmcrackan eb39f994e1 Merge pull request #1006 from cbordeman/Friendly-name-filters-995
Fixed bug in main view quick filter binding.
2024-10-16 08:03:44 -04:00
Chris Bordeman c19833b34e Fixed bug in main view quick filter binding. 2024-10-15 21:46:22 -04:00
Robert McRackan 6dcf456d06 fix ver 2024-10-15 13:20:58 -04:00
rmcrackan 8a87462cf5 Merge pull request #997 from cbordeman/Friendly-name-filters-995
Implemented Name on Quick Filters.
2024-10-15 13:09:38 -04:00
Chris Bordeman 9da2a44eff Small fix 2024-10-15 00:13:40 -04:00
Chris Bordeman 7af8d8aa70 Remove some warnings. 2024-10-15 00:07:57 -04:00
Chris Bordeman 4801f37e7c Moved QuickFilters migration to AppScaffolding. 2024-10-14 23:59:05 -04:00
rmcrackan 4f5df44d40 Merge pull request #999 from cbordeman/Add-default-theme-setting
Implemented "System" option for theme.
2024-10-14 15:09:21 -04:00
Chris Bordeman 63e28b13c1 Implemented "System" option for theme. 2024-10-12 02:17:35 -04:00
Chris Bordeman f92b2b65b2 Implemented Name on Quick Filters. 2024-10-11 05:45:04 -04:00
rmcrackan f7a4a95e3b Merge pull request #994 from cbordeman/master
Disable shrink/expand on mouseover for all scrollbars.
2024-10-10 07:38:24 -04:00
Chris Bordeman 71b8e9e51c Disable shrink/expand on mouseover for all scrollbars. 2024-10-10 02:22:36 -04:00
Robert McRackan c6788ccb48 typo 2024-09-25 10:12:01 -04:00
Robert McRackan 0503ee1404 incr ver 2024-09-18 08:17:35 -04:00
rmcrackan 303192c6c3 Merge pull request #978 from muchtall/allow-UseCoverAsFolderIcon-on-linux
Allow Non-Windows installations to populate Windows folder artwork
2024-09-18 08:15:05 -04:00
muchtall 6e21e96aa2 Allow Non-Windows installations to populate Windows folder artwork (https://github.com/rmcrackan/Libation/issues/977) 2024-09-16 19:42:26 +00:00
Robert McRackan d1d0a7e487 db migration for IsFinished 2024-09-11 07:56:18 -04:00
Robert McRackan 2fd8ea91e1 New search field: Finished/IsFinished.
All work complete except db migration
2024-09-11 07:45:37 -04:00
Robert McRackan 92ee0b2e6d update dependencies 2024-09-11 07:20:54 -04:00
Robert McRackan f0b5ae1cdc New filter option: IsInSeries, InSeries 2024-09-09 14:22:05 -04:00
Robert McRackan eb659cc7d7 typo 2024-09-09 08:31:55 -04:00
Robert McRackan cdd5f229d3 incr ver 2024-09-05 09:12:19 -04:00
Robert McRackan 29edfb7c3f Merge branch 'master' of https://github.com/rmcrackan/Libation 2024-09-05 09:07:54 -04:00
Robert McRackan c0cb454d45 Make recursive file enumerations safer 2024-09-05 09:07:47 -04:00
rmcrackan 970a77c9e9 Merge pull request #946 from rmcrackan/dependabot/nuget/Source/LibationUiBase/SixLabors.ImageSharp-3.1.5
Bump SixLabors.ImageSharp from 3.1.4 to 3.1.5 in /Source/LibationUiBase
2024-07-22 14:05:40 -04:00
dependabot[bot] 3488c8e0f5 Bump SixLabors.ImageSharp from 3.1.4 to 3.1.5 in /Source/LibationUiBase
Bumps [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/SixLabors/ImageSharp/releases)
- [Commits](https://github.com/SixLabors/ImageSharp/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 17:45:08 +00:00
rmcrackan 5133720cc8 InstallOnMac.md updated instructions 2024-06-30 07:22:38 -04:00
rmcrackan 4150746f45 InstallOnMac.md typo 2024-06-29 08:08:57 -04:00
Robert McRackan 3a95e1e72f incr ver 2024-06-26 08:00:48 -04:00
rmcrackan c74e26e1af Update release.yml
Release titles: don't include "v" in front of version number
2024-06-26 07:54:11 -04:00
rmcrackan ae54c95d46 Update docker.yml
Rolling back docker build push action to attempt to fix build
2024-06-26 07:51:00 -04:00
Robert McRackan 56e6bd164b By user request, other Serilog.Sink packages are included for experimental use 2024-06-26 07:07:49 -04:00
rmcrackan 9b28bdceaa Merge pull request #923 from rmcrackan/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-17 16:33:34 -04:00
dependabot[bot] b7f7d9004d Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 14:23:33 +00:00
rmcrackan 1be0991e62 Add Audibly to FAQ 2024-06-07 13:27:18 -04:00
rmcrackan ff8a2e59c5 Update FrequentlyAskedQuestions.md 2024-05-27 11:48:48 -04:00
Robert McRackan 453904261b Bug fix. Audible's api acts weird when page count gets to ~400 2024-05-16 07:53:19 -04:00
Robert McRackan f9340db90a update references. audible api bugfix 2024-05-15 06:55:25 -04:00
Robert McRackan 5cd329dd26 refactor 2024-05-13 15:23:37 -04:00
Robert McRackan b2a882b79d Bug fix #904 -- navigation bug with new Accessibility feature 2024-05-13 15:17:17 -04:00
Robert McRackan 75df78a2f7 Add OSVersion to logs 2024-05-13 14:01:34 -04:00
Robert McRackan 3ad52cbecc docker to .net8 2024-05-09 10:01:38 -04:00
Robert McRackan 27b2fe741c Add accessibility 2024-05-07 10:39:30 -04:00
Robert McRackan d19fe2250c Add accessibility text to grid stoplight buttons 2024-05-06 21:56:00 -04:00
Robert McRackan d16d0c8de2 Merge branch 'master' of https://github.com/rmcrackan/Libation 2024-05-05 16:24:07 -04:00
Robert McRackan c213d5d9f6 Attempt to solve networking issue by disabling ipv6 2024-05-05 16:24:00 -04:00
rmcrackan c73a023572 Update FrequentlyAskedQuestions.md 2024-04-30 22:42:34 -04:00
rmcrackan 67389917fd Merge pull request #879 from rmcrackan/dependabot/nuget/Source/LibationUiBase/SixLabors.ImageSharp-3.1.4
Bump SixLabors.ImageSharp from 3.1.3 to 3.1.4 in /Source/LibationUiBase
2024-04-15 17:03:13 -04:00
dependabot[bot] b3264d5f42 Bump SixLabors.ImageSharp from 3.1.3 to 3.1.4 in /Source/LibationUiBase
Bumps [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/SixLabors/ImageSharp/releases)
- [Commits](https://github.com/SixLabors/ImageSharp/compare/v3.1.3...v3.1.4)

---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-15 20:27:24 +00:00
Robert McRackan 962d9b550f "Unable to load module" should just be a warning, not error 2024-04-15 08:03:49 -04:00
Robert McRackan 91ce7272ae Bug fix #874 : Chardonnay wasn't allowing custom directories ending in "Books" 2024-04-04 08:13:37 -04:00
rmcrackan 2f64ca6856 Merge pull request #870 from ScubyG/patch-1
Update InstallOnLinux.md
2024-03-22 07:37:00 -04:00
Davy Jones cfe5db436c Update InstallOnLinux.md
Fixed typo
2024-03-22 01:23:52 +00:00
rmcrackan 3653fc8094 Merge pull request #863 from rmcrackan/dependabot/github_actions/softprops/action-gh-release-2
Bump softprops/action-gh-release from 1 to 2
2024-03-11 10:45:44 -04:00
dependabot[bot] 662c0ec871 Bump softprops/action-gh-release from 1 to 2
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 14:20:12 +00:00
rmcrackan 44d39eabdb Merge pull request #861 from rmcrackan/dependabot/nuget/Source/LibationUiBase/SixLabors.ImageSharp-3.1.3
Bump SixLabors.ImageSharp from 3.1.2 to 3.1.3 in /Source/LibationUiBase
2024-03-05 11:45:59 -05:00
dependabot[bot] a0550d5c97 Bump SixLabors.ImageSharp from 3.1.2 to 3.1.3 in /Source/LibationUiBase
Bumps [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/SixLabors/ImageSharp/releases)
- [Commits](https://github.com/SixLabors/ImageSharp/compare/v3.1.2...v3.1.3)

---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-05 16:38:00 +00:00
Robert McRackan 14eaca6d45 Update dependency. Security update 2024-03-05 11:36:36 -05:00
Robert McRackan 93ccc206ef Update dependencies 2024-03-04 14:02:42 -05:00
Robert McRackan d3f0fd711e Remove validation check: SeriesName is null . At least 1 exception found in the wild 2024-02-29 14:44:41 -05:00
rmcrackan 3f4604e877 Update FrequentlyAskedQuestions.md
reformat
2024-02-29 10:15:47 -05:00
rmcrackan c316709af8 Update FrequentlyAskedQuestions.md
Classic vs Chardonnay
2024-02-29 10:15:01 -05:00
Robert McRackan 221d5c7f1c paypal link 2024-02-19 10:32:26 -05:00
Robert McRackan 5a86a1a27b Typo 2024-02-13 21:42:24 -05:00
rmcrackan dc5e55de68 Merge pull request #848 from patienttruth/master
Update Docker.md
2024-02-13 07:22:33 -05:00
patienttruth ee37864a42 Update Docker.md
Included an explanation that you may have to manually input the InProgress variable.
2024-02-13 06:49:33 +00:00
Robert McRackan efe347667c Better error reporting in audible api -- incl. inner exceptions 2024-01-20 21:44:29 -05:00
Robert McRackan f27a18bdbb Better error reporting in audible api 2024-01-19 09:30:48 -05:00
Robert McRackan d1834659d9 incr ver 2024-01-12 09:08:14 -05:00
Robert McRackan 7842b521d7 Update dependencies. Notes about upgrading 2024-01-12 07:44:40 -05:00
Robert McRackan 0822f0229d Revert Avalonia. Bug in 11.0.6 2024-01-04 10:03:33 -05:00
Robert McRackan 26aee4d29d Merge branch 'master' of https://github.com/rmcrackan/Libation 2024-01-03 11:43:59 -05:00
Robert McRackan 17a80a23a8 AAXClean upgrade to .net8 2024-01-03 11:43:45 -05:00
rmcrackan e26fc9ca62 Merge pull request #808 from rmcrackan/dependabot/github_actions/actions/download-artifact-4
Bump actions/download-artifact from 3 to 4
2023-12-15 09:50:25 -05:00
rmcrackan a03ccf1143 Merge pull request #807 from rmcrackan/dependabot/github_actions/actions/upload-artifact-4
Bump actions/upload-artifact from 3 to 4
2023-12-15 09:50:11 -05:00
dependabot[bot] bb8dd615db Bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 14:08:26 +00:00
dependabot[bot] 9022a2889f Bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 14:08:20 +00:00
rmcrackan ef049a3b02 Merge pull request #805 from rmcrackan/dependabot/github_actions/actions/setup-dotnet-4
Bump actions/setup-dotnet from 3 to 4
2023-12-05 10:01:34 -05:00
dependabot[bot] 77409750aa Bump actions/setup-dotnet from 3 to 4
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 3 to 4.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-05 14:14:17 +00:00
Robert McRackan 1702130b01 Upgrade to .net8 2023-11-15 19:57:39 -05:00
Robert McRackan b6d1a7e3ba Upgrade to .net8 2023-11-15 19:53:26 -05:00
Robert McRackan 2907ba5c13 Add FAQ 2023-10-20 08:27:20 -04:00
Robert McRackan 6df6c79ac8 New locale: Brazil 2023-10-18 22:33:24 -04:00
rmcrackan 3a9ca5d827 Merge pull request #779 from rmcrackan/dependabot/github_actions/dwenegar/upload-release-assets-2
Bump dwenegar/upload-release-assets from 1 to 2
2023-10-13 14:41:26 -04:00
dependabot[bot] e1e663e327 Bump dwenegar/upload-release-assets from 1 to 2
Bumps [dwenegar/upload-release-assets](https://github.com/dwenegar/upload-release-assets) from 1 to 2.
- [Release notes](https://github.com/dwenegar/upload-release-assets/releases)
- [Commits](https://github.com/dwenegar/upload-release-assets/compare/v1...v2)

---
updated-dependencies:
- dependency-name: dwenegar/upload-release-assets
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-13 14:37:55 +00:00
Robert McRackan 4b00d5fd84 incr ver 2023-09-18 13:58:23 -04:00
rmcrackan 02dbf8aad0 Merge pull request #751 from Mbucari/master
Minor Bug Fixes
2023-09-18 13:56:53 -04:00
rmcrackan 8326389f5c Merge pull request #732 from rmcrackan/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-09-18 13:56:33 -04:00
dependabot[bot] 34535b3ce1 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 17:33:43 +00:00
rmcrackan 7e5366ab95 Merge pull request #738 from rmcrackan/dependabot/github_actions/docker/setup-buildx-action-3
Bump docker/setup-buildx-action from 2 to 3
2023-09-18 13:30:24 -04:00
dependabot[bot] 690de9bc5c Bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 17:25:20 +00:00
rmcrackan c976aa2bb2 Merge pull request #739 from rmcrackan/dependabot/github_actions/docker/login-action-3
Bump docker/login-action from 2 to 3
2023-09-18 13:25:07 -04:00
rmcrackan 27f659285d Merge pull request #740 from rmcrackan/dependabot/github_actions/docker/build-push-action-5
Bump docker/build-push-action from 4 to 5
2023-09-18 13:24:55 -04:00
rmcrackan 423a5e7720 Merge pull request #741 from rmcrackan/dependabot/github_actions/docker/setup-qemu-action-3
Bump docker/setup-qemu-action from 2 to 3
2023-09-18 13:24:44 -04:00
Michael Bucari-Tovo 9152e12fe1 Fix #748 2023-09-18 10:14:54 -06:00
Michael Bucari-Tovo f471c53139 Fix #734 2023-09-18 10:08:15 -06:00
dependabot[bot] 66d055bb90 Bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 14:50:49 +00:00
dependabot[bot] 2bbb35363a Bump docker/build-push-action from 4 to 5
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 14:50:46 +00:00
dependabot[bot] 1d3687cf9e Bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 14:50:42 +00:00
Mbucari daf925157f Update AppScaffolding.csproj 2023-09-02 07:59:30 -06:00
Mbucari 40eec9e674 Merge pull request #730 from Mbucari/master
Fix #729
2023-09-01 18:46:42 -06:00
MBucari 6f0782053e Restore PDF functionality (#729) 2023-09-01 16:30:35 -06:00
Michael Bucari-Tovo 04ad033ba0 Remove appsettings.json from program files dir 2023-08-31 09:36:38 -06:00
Robert McRackan f12f8ba3ee incr ver 2023-08-30 15:00:20 -04:00
rmcrackan cc6feb21ff Merge pull request #728 from Mbucari/master
Minor Bug fixes
2023-08-30 14:46:16 -04:00
Michael Bucari-Tovo c4f2ec428d Allow Settings form resizing 2023-08-30 12:38:28 -06:00
Michael Bucari-Tovo 59689cb647 Update Dependencies 2023-08-30 11:59:40 -06:00
Michael Bucari-Tovo d7f3758ebc Add support for environment variables and unix shortcuts in appsettings.json path. 2023-08-30 11:47:50 -06:00
Mbucari 49775b019c Update InstallOnMac.md 2023-08-30 09:06:57 -06:00
Michael Bucari-Tovo e55e969349 Fix issue with save button being disabled. 2023-08-30 08:59:20 -06:00
Mbucari 42a93bfac1 Update AAXClean 2023-08-27 19:47:32 -06:00
Mbucari f86c77a546 Create directory before trying to create appsettings.json 2023-08-27 19:47:00 -06:00
MBucari 88c35e2a56 Add scrolling view to settings window panels (#720) 2023-08-23 20:25:30 -06:00
MBucari b405e8b6b2 Remove unised response groups 2023-08-20 11:40:24 -06:00
MBucari 92d283187d Add support for locating mp3 audiobooks 2023-08-20 11:38:43 -06:00
Robert McRackan 51b8cfe71f incr ver 2023-08-17 14:03:40 -04:00
rmcrackan c80da5357b Merge pull request #716 from Mbucari/master
Fix NRE when item has no category ladders (#715)
2023-08-17 14:02:43 -04:00
Michael Bucari-Tovo 736d7c4a5f Fix NRE when item has no category ladders (#715) 2023-08-17 11:55:28 -06:00
Robert McRackan f175b7592e incr major ver 2023-08-12 21:45:11 -04:00
rmcrackan 415e6e7bc6 Merge pull request #710 from Mbucari/master
Batch of Fixxed Issues
2023-08-12 21:20:37 -04:00
MBucari d6a413e8d9 Update Avalonia to 11.0.3 2023-08-12 17:30:08 -06:00
Mbucari 3049de6246 Merge branch 'rmcrackan:master' into master 2023-08-12 17:20:09 -06:00
MBucari fb9b4eb77e Update audio duration on library scan (#707) 2023-08-12 17:19:56 -06:00
MBucari e65b6c76a8 Add ability to preview templates on user's books (#700)
Add template editor menu items to main grid context menu
2023-08-12 17:12:50 -06:00
Mbucari 167a021eb1 Change glass icon shading 2023-08-11 10:55:31 -06:00
Mbucari ff3ac2d6fd Fix MessageBox crash when UI has no Screens (#708) 2023-08-11 09:04:05 -06:00
Robert McRackan f733079a49 remove Moq 2023-08-10 15:24:43 -04:00
Mbucari 893d68190d Lazy load cover to improve startup time 2023-08-10 10:18:28 -06:00
Mbucari 97f94d8782 Add custom column widths to chardonnay 2023-08-07 15:54:50 -06:00
Mbucari 4b2ce0c2d1 Brighter stoplight colors (#702) 2023-08-07 11:31:22 -06:00
Mbucari ee00417c6f Add white glow to libation_glass.svg (#701) 2023-08-07 11:31:03 -06:00
Robert McRackan 768afd8ecd incr ver 2023-08-04 11:57:15 -04:00
rmcrackan 32c3fa85ce Merge pull request #699 from Mbucari/master
Fix broken template editor (#698)
2023-08-04 11:55:30 -04:00
Mbucari 6986c8f018 Fix broken template editor (#698) 2023-08-04 09:26:51 -06:00
Robert McRackan f69c2b1cfc incr ver 2023-08-02 21:55:19 -04:00
rmcrackan b11675c36a Merge pull request #696 from Mbucari/master
Bug fixes
2023-08-02 21:52:57 -04:00
Mbucari 379c2ed62d Fix account nickname retrieval (#629) 2023-08-02 13:54:49 -06:00
Mbucari 7c8489b52f Fix walkthrough causing freeze (#695) 2023-08-02 13:15:58 -06:00
rmcrackan c61a863edd Merge pull request #694 from Mbucari/master
Fix DPI scaling bug (#692)
2023-08-01 15:03:21 -04:00
Mbucari 1d54f32ef3 Fix DPI scaling bug (#692) 2023-08-01 11:55:23 -06:00
rmcrackan fabe4afd94 Merge pull request #691 from Mbucari/master
Fix #686 and enable nullable in FileManager and LibationFileManager
2023-07-30 20:22:55 -04:00
MBucari 61efa3c0c1 Update dependencies 2023-07-30 14:00:12 -06:00
MBucari fe70daf0bc Update avalonia to v11.0.1 2023-07-30 13:54:44 -06:00
MBucari 34033e7947 Enable Nullable 2023-07-30 13:31:57 -06:00
MBucari e8c63e9a6e Fix UI control overlapping label (#686) 2023-07-30 13:15:43 -06:00
Robert McRackan 9315165f80 update dependencies 2023-07-21 21:21:48 -04:00
Robert McRackan ce624399ba incr ver 2023-07-19 07:48:11 -04:00
rmcrackan 63e9700c4a Merge pull request #682 from Mbucari/master
Fix #680 and Add category ladders
2023-07-19 07:46:05 -04:00
Mbucari 914e574bf8 Improve GridView
- Remove LongDescription
- Description has the full description
- Better MyRating updating
2023-07-18 16:18:01 -06:00
Mbucari b94f9bbc15 Fix grid update bug 2023-07-18 16:11:22 -06:00
Mbucari 4e34834c35 Fix category search indexing 2023-07-18 16:00:06 -06:00
Mbucari 3211b2dc85 Improved Category Ladders 2023-07-18 15:51:02 -06:00
Mbucari ea6adeb58f Add category ladders 2023-07-17 16:50:45 -06:00
Mbucari 90eccbf2f6 Fix FilePathCache NRE (#680) 2023-07-17 08:55:55 -06:00
rmcrackan 668cd7dba8 Merge pull request #679 from Mbucari/master
Mp3 embedded cuesheet and raw metadata
2023-07-16 14:32:49 -04:00
MBucari c08b2b575c UI Tweak 2023-07-16 10:57:17 -06:00
MBucari 07eaa48e10 Save raw json metadata 2023-07-16 10:54:05 -06:00
MBucari 3cf5fc1d99 Add mp3 embedded cuesheet (#677) 2023-07-15 10:44:31 -06:00
Robert McRackan 15ad753fa1 update dependencies 2023-07-14 20:58:26 -04:00
rmcrackan 75b984bdb2 Merge pull request #678 from Mbucari/master
Fix quick filter not being applied on startup
2023-07-14 20:53:57 -04:00
Mbucari f586d1d59f Fix quick filter not being applied on startup 2023-07-13 11:00:05 -06:00
Mbucari cb91a591f0 inc ver 2023-07-13 09:58:45 -06:00
Mbucari 0c0c556c6a Merge pull request #674 from Mbucari/master
Fix #673
2023-07-13 09:31:28 -06:00
MBucari ff63b73c09 Fix #673 2023-07-13 09:30:02 -06:00
Mbucari c1d56adbd2 Add groupbox title 2023-07-12 21:29:00 -06:00
rmcrackan bcd99fd208 Merge pull request #670 from Mbucari/master
Add products grid scaling setting
2023-07-12 21:10:51 -04:00
Mbucari d1df10d060 Add products grid scaling setting
- Add Grid Scaling Settings
- Add WinForms DPI migration to remove stored form sizes
- Add textbox clear button
2023-07-12 15:32:37 -06:00
Mbucari 1fa415628f Update ProductsGrid.cs 2023-07-10 11:39:33 -06:00
rmcrackan a83fe9e532 Merge pull request #667 from Mbucari/master
Fix setting Panel2MinSize min width bug (#666)
2023-07-10 11:19:45 -04:00
Mbucari f85462ffec Fix setting Panel2MinSize min width bug (#666) 2023-07-10 09:11:38 -06:00
Robert McRackan 156349c293 incr ver 2023-07-10 09:26:26 -04:00
rmcrackan 5976706e40 Merge pull request #664 from Mbucari/startup-2
New settings, context menu, and performance improvements
2023-07-10 09:25:13 -04:00
Mbucari 1e40180f0c Fix unit test 2023-07-09 16:42:08 -06:00
Mbucari 7d09728e6b Add Re-download context menu item 2023-07-09 16:26:58 -06:00
Mbucari 4899ef3007 Add new settings and settings dialog help tips
Add CombineNestedChapterTitles setting (#663)
Add SaveMetadataToFile setting
Add extended setting descriptions for select options
2023-07-09 16:07:13 -06:00
Mbucari 296c2b43eb Remove extra library load and move comments to Main 2023-07-09 10:10:00 -06:00
Mbucari 932472cb91 Add full context menu to call columns 2023-07-09 09:53:28 -06:00
Mbucari 1bf86b05ec Download high quality cover art 2023-07-09 09:35:40 -06:00
Mbucari 5d5e3a6671 improve startup time 2023-07-09 09:23:58 -06:00
Robert McRackan 9720a573c7 incr ver 2023-07-07 20:27:57 -04:00
rmcrackan 1cf01aa92a Merge pull request #660 from Mbucari/master
Crash logging to chardonnay
2023-07-07 20:27:09 -04:00
Mbucari 4df9e5abbf Add unhandled error handling and crash logging to chardonnay 2023-07-07 14:14:12 -06:00
Mbucari 9243aa47e7 Upgrade Avalonia to v11.0.0 2023-07-07 14:13:54 -06:00
rmcrackan c69f41a2a6 Merge pull request #659 from Mbucari/master
Fix classic scaling on high dpi displays
2023-07-07 08:06:22 -04:00
Mbucari 27c74e52ca Fix classic scaling on high dpi displays 2023-07-06 21:34:29 -06:00
Robert McRackan bfa7f5cca9 Bug fix #657 : Settings dialog size was recently changed. Save and Cancel buttons were pushed outside of the dialog's bounds 2023-07-06 09:27:52 -04:00
rmcrackan 22a3dcbc1f Merge pull request #656 from Mbucari/master
Fix query parsing tags with underscores (#655)
2023-07-06 09:16:20 -04:00
Mbucari ec9d11cf52 Fix query parsing tags with underscores (#655) 2023-07-05 15:47:37 -06:00
Mbucari fbc29dfb0a Set Variety correctly 2023-07-04 09:58:39 -06:00
Robert McRackan 03d30ff6af incr. ver. 2023-07-03 22:06:00 -04:00
rmcrackan ecfe0dc033 Merge pull request #651 from Mbucari/master
Overhaul LibationCli and add Download Quality Option
2023-07-03 21:57:04 -04:00
Mbucari f2d475a9b0 Add audiobookshelf tags for m4b and mp3
Fix the following tag fields so they are correctly parsed and displayed in audiobookshelf:
Language
Publisher
Series name and number
ASIN
2023-07-03 15:57:11 -06:00
Mbucari 86124fc609 Address comments 2023-07-03 10:01:25 -06:00
Mbucari db2b10d2a4 Performance improvement 2023-07-03 07:04:29 -06:00
Mbucari 83402028fd Update Avalonia 2023-07-02 19:27:58 -06:00
Mbucari 423b5312f7 Add setting to choose downloaded audio quality ((#648) 2023-07-02 19:19:28 -06:00
Mbucari 3be7d8e825 Minor cli edits and fix potential deadlock 2023-07-02 18:29:36 -06:00
Mbucari 29803c6ba0 Overhaul LibationCli
Add version verb with option to check for upgrade
Add Search verb to search the library
Add export file type inference
Add more set-status options
Add console progress bar and ETA
Add processable option to liberate specific book IDs
Scan accounts by nickname or account ID
Improve startup performance for halp and on parsing error
More useful error messages
2023-07-02 15:01:10 -06:00
Mbucari bb05847b25 Improve finding audio file by ID 2023-07-02 14:08:27 -06:00
Robert McRackan 5219ad53e1 incr ver 2023-07-01 21:34:36 -04:00
Mbucari 30aa691aae Merge pull request #646 from Alanoll/feat-add-book-subtitles
feat: add Book subtitle capturing so TitleShort reflects titles better
2023-07-01 12:47:03 -05:00
Mbucari 83fa73cef5 Integrate new Title and Subtitle properties into Libation 2023-06-29 21:06:54 -06:00
Alanoll 2195574422 feat: add Book subtitle capturing so TitleShort reflects titles better 2023-06-26 12:18:15 -05:00
Robert McRackan 74ce408c8b incr ver 2023-06-25 21:27:59 -04:00
rmcrackan 85be15b843 Merge pull request #642 from Mbucari/master
Bug fixes and minor features
2023-06-25 21:26:24 -04:00
MBucari b4b85cd485 Change the default file timestamp source 2023-06-25 17:28:26 -06:00
Mbucari 0093968537 Merge branch 'rmcrackan:master' into master 2023-06-25 15:25:52 -06:00
MBucari 1b09b1fd48 Remove multispace instances from template filenames (#637) 2023-06-25 15:14:10 -06:00
MBucari ac87d70613 Add options to set file created/modified timestamps (#638) 2023-06-25 14:07:39 -06:00
MBucari a5d98364fa Enable auto-downloading (#636) 2023-06-25 11:12:52 -06:00
MBucari ca0e639a19 Commit account edits before saving (#639) 2023-06-25 11:11:58 -06:00
Robert McRackan b0e3022988 incr ver 2023-06-15 21:40:35 -04:00
rmcrackan 6765c2bfa7 Merge pull request #633 from Mbucari/master
User series order float (#632)
2023-06-15 21:38:02 -04:00
Mbucari 94d3742317 Update NamingTemplates.md 2023-06-15 12:33:58 -06:00
Mbucari bd3e833dc1 Use series order float (#632)
Add decimal formatter to number tag types
2023-06-15 10:42:36 -06:00
rmcrackan a386ace0e6 Update NamingTemplates.md
Add \<account nickname\>
2023-06-14 14:06:21 -04:00
rmcrackan 8221d7e202 Merge pull request #631 from Mbucari/master
Add features #626 and #627 and Fix #628
2023-06-14 14:03:24 -04:00
Robert McRackan fa92946d20 incr ver 2023-06-14 14:02:50 -04:00
Mbucari 6d13325c4f Add <account nickname> tag (#629) 2023-06-14 11:56:38 -06:00
Mbucari 7a9c6720c7 Fix Stupid 2023-06-14 11:35:11 -06:00
Mbucari 697f797509 Remove debug code 2023-06-14 11:16:53 -06:00
Mbucari ec9854212a Write error info to StdErr (#626) 2023-06-14 10:58:37 -06:00
Mbucari 46f6ba1710 Add feature #627 and fix bug #628
- Feature: Option to overwrite existing audio files when moving to Books
- Bugfix: Do not set liberated status if moving files fails.
2023-06-14 10:51:43 -06:00
rmcrackan 7347244f0a Merge pull request #630 from CLHatch/patch-1
Update Advanced.md
2023-06-14 07:17:45 -04:00
CLHatch c29c4c470c Update Advanced.md
M4B files use the `@wrt` instead of `TCOM` tag for "composer".
2023-06-14 02:33:49 -05:00
rmcrackan ee51fd9da6 Merge pull request #625 from Mbucari/master
Refactor LibationSearchEngine
2023-06-13 12:39:42 -04:00
Mbucari 2c4705de6e Address #625 comments and refactor 2023-06-13 09:05:17 -06:00
Mbucari b4aa220051 Refactor LibationSearchEngine 2023-06-12 14:02:55 -06:00
Robert McRackan 4ab6da132b Bug fix #621 2023-06-12 10:13:40 -04:00
Mbucari b006429a53 Fix #621 (#624) 2023-06-11 21:05:42 -06:00
Robert McRackan 54d157d244 another tag fail. incr ver 2023-06-11 17:07:03 -04:00
Robert McRackan a4dfdf80e4 Merge branch 'master' of https://github.com/rmcrackan/Libation 2023-06-11 17:03:55 -04:00
Robert McRackan d8c90bc745 incr ver 2023-06-11 17:03:35 -04:00
Mbucari 46accddd2d Merge pull request #623 from Mbucari/master
Redesign query sanitizer (#618)
2023-06-11 11:51:13 -06:00
Mbucari f40ecbc07e Merge branch 'rmcrackan:master' into master 2023-06-11 11:33:28 -06:00
Mbucari 536982cb5f Remove obsolete code 2023-06-11 09:44:30 -06:00
Mbucari ea3d96329b Add query sanitization unit tests 2023-06-11 09:44:21 -06:00
rmcrackan e87fcbb16f Update Settings documentation 2023-06-11 10:04:00 -04:00
Mbucari 541cf79b6f Redesign query sanitizer (#618) 2023-06-10 15:08:50 -06:00
Robert McRackan 55fa82f92e New incr ver. Previous Tag attempt did generate builds; did not draft a new release 2023-06-09 11:49:59 -04:00
Robert McRackan 4a0c2b2180 Bug fix #618 2023-06-09 11:27:40 -04:00
Mbucari c77fe5d561 Add Asin query tokenizer 2023-06-08 14:23:39 -06:00
Robert McRackan 359d082ffd incr ver 2023-06-03 15:06:12 -04:00
rmcrackan 017bdba404 Merge pull request #616 from Mbucari/master
Fix #612 and update Avalonia to v11-rc1
2023-06-03 15:04:56 -04:00
Mbucari d4bf13b3fd Update Hangover Avalonia to v11-rc1 2023-06-03 00:30:02 -06:00
Mbucari 87b695b2de Merge branch 'rmcrackan:master' into master 2023-06-03 00:01:10 -06:00
Mbucari 222b16113e Update NamingTemplates.md 2023-06-03 00:00:01 -06:00
Mbucari 75c07c3209 Fix SavePodcastsToParentFolder setting (#612) 2023-06-02 23:54:32 -06:00
Mbucari e640edee7f Use proper key name 2023-06-02 23:53:48 -06:00
Mbucari 6c48fc1f5e Update avalonia ro v11-RC1 2023-06-02 23:39:16 -06:00
Mbucari e5708a382b Use new synchronous UI invoker 2023-06-02 23:21:55 -06:00
Robert McRackan da9cb3371f incr ver 2023-05-23 13:06:09 -04:00
rmcrackan 91d0f8020e Merge pull request #606 from Mbucari/master
Corectly read and write locales
2023-05-23 13:04:35 -04:00
Mbucari 156726ca95 Corectly read and write locales 2023-05-23 09:41:28 -06:00
rmcrackan 3dad4c194b Update README.md 2023-05-20 23:12:23 -04:00
Mbucari 6025a7538a Merge pull request #604 from Mbucari/master
Fix rpm upgrade
2023-05-19 16:39:04 -06:00
Mbucari 824f65baae Fix rpm upgrade 2023-05-19 16:37:00 -06:00
Mbucari 9372a7318b inc ver 2023-05-19 13:46:26 -06:00
Mbucari ddd032c16d Fix null string in integer fields 2023-05-19 13:36:22 -06:00
Mbucari 9aaf523240 Update InstallOnMac.md 2023-05-19 13:07:33 -06:00
Mbucari 8cbdeb38fa Update InstallOnLinux.md 2023-05-19 13:05:29 -06:00
Mbucari a9258a1811 Update GettingStarted.md 2023-05-19 13:01:25 -06:00
Robert McRackan 0dbc42c407 incr ver 2023-05-19 14:43:17 -04:00
rmcrackan 2c91de1b3b Merge pull request #603 from Mbucari/master
New searches, new linux release, new Avalonia build
2023-05-19 14:23:05 -04:00
Mbucari 607cd07b74 Change IInteropFunctions.ReleaseIdentifier to ReleaseIdString 2023-05-19 12:08:22 -06:00
Mbucari 64d080336c Use correct package manager 2023-05-19 11:30:09 -06:00
Mbucari fd510861c6 Add AbsentFromLastScan (#601) LastDownloaded (#602) search 2023-05-19 11:09:57 -06:00
Mbucari 3fdfbb9e26 Improve episode sequence detection (#600) 2023-05-19 11:07:42 -06:00
Mbucari 3e74898dac Merge branch 'rmcrackan:master' into master 2023-05-19 09:31:47 -06:00
Mbucari d6fe3013ab RPM build 2023-05-19 09:30:11 -06:00
Robert McRackan 265794bae0 update dependencies 2023-05-19 11:24:47 -04:00
Mbucari 7586f7a159 Upgrade Avalonia to v11.0.0-preview8 2023-05-15 12:58:45 -06:00
Mbucari 5dfddfb549 Use avres DataGrid theme and only replace DataGridColumnHeader 2023-05-15 12:51:46 -06:00
Mbucari 98bb06378a Update Avalonia to v11.0.0-preview8 2023-05-15 10:54:56 -06:00
Robert McRackan 429367d21c incr ver 2023-04-17 21:39:18 -04:00
Mbucari ea9e36fd76 Merge pull request #588 from Mbucari/master
Use old activation bytes if present.
2023-04-17 19:34:26 -06:00
MBucari fe534b335b Update dependencies 2023-04-17 19:32:52 -06:00
MBucari 6db3a8fbf3 Use old activation bytes if present. 2023-04-17 16:09:47 -06:00
Robert McRackan 48c69a1339 Can log to zip files with new ZipFile sink 2023-04-15 15:55:53 -04:00
rmcrackan 1ab882f327 Merge pull request #587 from Mbucari/master
Serilog log to zip file
2023-04-15 15:53:41 -04:00
MBucari 019b110a8a Fix #585 2023-04-15 13:43:50 -06:00
MBucari 9e14169e15 Update dependencies 2023-04-15 13:39:43 -06:00
MBucari e08a68219d Add Serilog.Sinks.ZipFile to write logs into a zip file 2023-04-15 12:45:20 -06:00
Mbucari af24c6e07b Merge branch 'rmcrackan:master' into master 2023-04-15 10:58:06 -06:00
Robert McRackan e31847e669 Incr. ver. 2023-04-14 14:38:45 -04:00
Mbucari c4f55d2ad1 Change "Click here" link verbiage 2023-04-14 11:37:22 -06:00
rmcrackan 1439e38cb0 Merge pull request #584 from Mbucari/master
Web Browser Login for Windows
2023-04-14 13:33:23 -04:00
Mbucari 4456432116 Add WebLoginDialog for Windows Chardonnay 2023-04-13 19:16:32 -06:00
Mbucari df2936e0b6 Use WebLoginDialog as primary login method on Win10+ 2023-04-13 09:10:13 -06:00
Mbucari 53b5c1b902 Fix rare bug where episode may not sort beneath its parent 2023-04-11 14:43:01 -06:00
Mbucari 82fba7e752 Grid refresh performance and behavior improvements 2023-04-11 14:33:45 -06:00
rmcrackan 1a95f2923b Merge pull request #579 from Mbucari/master
Bug fixes and more shared code moved to UI base
2023-04-10 22:47:24 -04:00
Mbucari 1939aae81c Simplify and comment 2023-04-10 19:50:30 -06:00
Mbucari 9a663fda15 Filtering bugfix 2023-04-10 17:08:09 -06:00
Mbucari 84b2996102 Merge branch 'rmcrackan:master' into master 2023-04-10 16:17:24 -06:00
Mbucari af8e1cd5ef Change episode default sorting to SeriesOrder descending 2023-04-10 16:17:10 -06:00
Mbucari 8a1b375f0d Fix #574 (for realsies this time) 2023-04-10 15:00:32 -06:00
Mbucari 6800986f25 Update GridEntryBindingList to behave move like Chardonnay 2023-04-10 14:10:50 -06:00
Mbucari 6110b08d16 Fix typo 2023-04-10 13:05:50 -06:00
Mbucari 666b5d83df Move filter query and RowComparer into UI base 2023-04-10 13:05:38 -06:00
rmcrackan 7db5a34f1b Merge pull request #577 from Mbucari/master
Fixed your issues
2023-04-10 13:14:05 -04:00
Mbucari e52772826a Merge branch 'rmcrackan:master' into master 2023-04-09 17:45:38 -06:00
Mbucari 8ea9b2abc6 Fix #574 2023-04-09 17:41:24 -06:00
Mbucari c10bb276f5 Fix #575 2023-04-09 17:41:10 -06:00
Mbucari 9dcb3b3a25 Slight chardonnay refactor and UI tweak 2023-04-09 17:39:31 -06:00
Robert McRackan d857882220 Bug fix: logins 2023-04-07 19:58:13 -04:00
rmcrackan d731db4036 Update Docker.md 2023-04-05 08:02:42 -04:00
rmcrackan ca5b40b176 Merge pull request #571 from Mbucari/master
Chardonnay UI Refinements and Refactor
2023-04-05 08:00:44 -04:00
MBucari b29ec26f63 Remove useless interface 2023-04-04 22:38:02 -06:00
MBucari 7569b01bd0 MacOS Compatibility 2023-04-04 22:26:13 -06:00
MBucari 6465b0a885 Fix possible NRE 2023-04-04 19:17:43 -06:00
Mbucari 5e99cb6f02 Refine dialog layouts and presentation 2023-04-04 19:08:52 -06:00
Mbucari d737cd2199 Improve LinkLabel control 2023-04-04 12:10:23 -06:00
Mbucari 2d2907e076 Refactor settings dialog 2023-04-04 11:18:28 -06:00
Mbucari 05c454dce4 Fix directory select controls 2023-04-04 10:49:27 -06:00
rmcrackan e64a9d2adf Merge pull request #566 from Mbucari/master
Reattach event handlers
2023-04-03 16:23:10 -04:00
Mbucari 6252f015b3 Reattach event handlers 2023-04-03 14:09:22 -06:00
rmcrackan 7ada0082a9 Merge pull request #565 from Mbucari/master
About Dialog, mac menus, and hotkeys
2023-04-03 15:54:40 -04:00
Mbucari 826e53c9cb Remove assemblies add acknowledgements to About 2023-04-03 13:34:20 -06:00
Mbucari 2248d7b24e Sort episodes by column beneath their parents 2023-04-02 21:28:55 -06:00
Mbucari 69918c2587 Cleanup 2023-04-02 21:28:37 -06:00
Michael Bucari-Tovo 1991bf5b4d Add more info to About dialog 2023-04-02 18:16:01 -06:00
MBucari 756d387238 UI Tweaks and new application hotkeys 2023-04-02 15:08:03 -06:00
MBucari 8d73f5cc7e Add About dialog 2023-04-02 13:27:51 -06:00
MBucari 4a65d6bbd3 Add native menu for mac and refactor MainWindow 2023-04-01 23:58:22 -06:00
Robert McRackan 10a1b56b3c incr ver 2023-03-31 16:39:13 -04:00
Robert McRackan 66fb392b7f Merge branch 'master' of https://github.com/rmcrackan/Libation 2023-03-31 16:16:23 -04:00
Robert McRackan 49ef96055c update dependencies 2023-03-31 16:16:21 -04:00
rmcrackan cb4a209f69 Merge pull request #564 from Mbucari/master
Fix #563 and probably fix #534
2023-03-31 14:27:43 -04:00
Mbucari 255e18eb5e Fix external login failure error (#563) 2023-03-31 12:00:20 -06:00
Mbucari 7e1ec47b46 Tweak AccessKeyHandler 2023-03-31 11:59:48 -06:00
MBucari 40c725b8c2 Merge branch 'master' of https://github.com/Mbucari/Libation 2023-03-30 19:58:19 -06:00
MBucari 5d0937dc48 Add support for custom access keys 2023-03-30 19:57:39 -06:00
Robert McRackan bff81bfc4b update paypal links 2023-03-30 09:44:20 -04:00
MBucari aa7c159985 Define window dimensions 2023-03-29 19:44:33 -06:00
Robert McRackan 012d94a146 incr ver 2023-03-29 18:02:33 -04:00
Mbucari 22bd1ed121 Fix autoscan bug 2023-03-29 15:54:46 -06:00
Mbucari c832f26b08 Merge pull request #561 from Mbucari/master
Try fix #560
2023-03-29 15:40:52 -06:00
Mbucari efd73d334e inv ver 2023-03-29 15:39:25 -06:00
Mbucari 0db3ee6fd7 Fix library scan bug 2023-03-29 15:38:57 -06:00
Robert McRackan 6aaf4f63d1 incr major ver 2023-03-29 15:58:57 -04:00
rmcrackan ab392a9285 Merge pull request #558 from Mbucari/master
Refined Walkthrough
2023-03-29 15:54:15 -04:00
Mbucari efc9ff4bd8 Disable buttons on new row 2023-03-29 13:31:39 -06:00
Mbucari a52b466c85 Fix QuickFilter Walkthrough 2023-03-29 13:17:31 -06:00
Mbucari 5611431abf Quick Filters display moveup and movedown buttons appropriately 2023-03-29 13:06:18 -06:00
Mbucari a75932d1f4 Refine Walkthrough 2023-03-29 11:35:17 -06:00
Mbucari 6c8464b650 Use HashSet 2023-03-29 11:32:07 -06:00
rmcrackan ba4a1c5a51 Merge pull request #554 from Mbucari/master
Bug fixes and guided tour
2023-03-28 16:49:49 -04:00
Mbucari 3681c0f18f Final walkthrough tweaks 2023-03-28 14:08:51 -06:00
Mbucari e365ba7296 Use AvaloniaList properties 2023-03-28 13:29:07 -06:00
Mbucari 2afb5365dd Add search and quick filters to walkthrough 2023-03-28 12:30:05 -06:00
Mbucari 00cf7693d5 Add code comments 2023-03-28 10:02:22 -06:00
MBucari dac6877a06 Fix #556 2023-03-28 07:09:46 -06:00
MBucari 36005508a1 Allow users to cancel walkthrough 2023-03-27 20:24:15 -06:00
MBucari d9e27fd32e Bring cover viewer to front 2023-03-27 19:56:50 -06:00
MBucari d86bcbb414 Add usings 2023-03-27 19:52:26 -06:00
MBucari 00cbab5b58 Update window title 2023-03-27 19:51:10 -06:00
MBucari 807725f6ff Replace editable DataGridTextColumn with TextBox (#552) 2023-03-27 19:40:23 -06:00
MBucari ec9356b36e Do not import orphaned episodes (#553) 2023-03-27 18:58:43 -06:00
MBucari add31024da Improve book availability detection (#551) 2023-03-27 17:53:25 -06:00
MBucari 27d2ada5a4 Don't warn for blank password with external login 2023-03-27 17:23:46 -06:00
Mbucari 702219ee69 Add guided walkthrough 2023-03-27 16:18:21 -06:00
Mbucari cdf1a01457 Do not launch settings dialog after installation 2023-03-27 13:18:37 -06:00
Mbucari a71ccbac6e Add Series Order column 2023-03-27 12:13:56 -06:00
Mbucari f8c6b836c3 Merge branch 'rmcrackan:master' into master 2023-03-27 11:15:19 -06:00
Michael Bucari-Tovo 090871f50d More migrations to Avalonia 11.0.0-preview6 2023-03-27 11:14:54 -06:00
Robert McRackan e62f01d2a3 Incr ver so bug fixes can be released. New features will also be announced in upcoming new major ver 2023-03-27 08:48:53 -04:00
Mbucari 68af6a5ebb Merge branch 'rmcrackan:master' into master 2023-03-26 21:04:53 -06:00
Michael Bucari-Tovo 8bba8538d5 Recheck for partially downloaded files. 2023-03-26 20:54:29 -06:00
rmcrackan 2cd9b86930 Merge pull request #549 from Mbucari/master
Lots of Bug Fixes and 2 New Features.
2023-03-26 22:54:08 -04:00
MBucari b876d90964 Remove AudibleApi from solution 2023-03-26 08:43:33 -06:00
Mbucari 49c91c273b Merge branch 'rmcrackan:master' into master 2023-03-26 08:08:36 -06:00
MBucari c07bc88493 Update AudibleApi 2023-03-25 21:18:38 -06:00
MBucari 397a516dc1 Fix (#548) 2023-03-25 21:18:38 -06:00
Robert McRackan 1c2b51aa83 update dependencies 2023-03-25 22:25:37 -04:00
Mbucari fc6f494f0d Add dark mode support 2023-03-25 16:33:11 -06:00
Mbucari 7289459170 Migrate to Avalonia 11.0.0-preview6 2023-03-22 13:44:25 -06:00
Mbucari ed6f741a65 Fix SettingsFileIsValid 2023-03-22 11:46:11 -06:00
Mbucari 1783da3e2d Ensure series and episode DateAdded is never default (#543) 2023-03-22 11:02:57 -06:00
Mbucari e7eac7bed3 Log DTO items even if validation fails 2023-03-22 11:00:51 -06:00
MBucari 9ae1f0399b Add SeriesViewDialog 2023-03-22 08:28:20 -06:00
MBucari 784ab73a36 Add context menu to Series grid entries (#536) 2023-03-19 12:04:51 -06:00
MBucari 99687e968e Create books directory if not found (#542) 2023-03-19 10:19:38 -06:00
MBucari 565c84c4ab Add series # to grid display (#529) 2023-03-17 22:11:04 -06:00
MBucari 18cf20ecad All books that pass the filter are counted as "visible" (#536) 2023-03-17 19:52:47 -06:00
MBucari 2725340994 Suppress VisibleCountChanged firing when updating grid 2023-03-17 17:51:39 -06:00
MBucari 56de1e7659 Preserve "expanded" status when updating library 2023-03-17 17:47:27 -06:00
Mbucari fd16e97632 When book is unavailable, check other accounts (#535) 2023-03-17 14:06:02 -06:00
Robert McRackan 36076242a7 Bug fix #532 : Possible rull ref exception for pre-amazon germany 2023-03-15 13:49:11 -04:00
Robert McRackan 718e6c14d0 update dependency 2023-03-14 22:10:55 -04:00
rmcrackan eb61ba3d69 Merge pull request #531 from Mbucari/master
Bug fixes and performance improvements
2023-03-14 07:53:25 -04:00
MBucari defabf7356 Use new AudibleApi methods 2023-03-13 21:00:25 -06:00
Mbucari 1149c10cf1 Merge branch 'rmcrackan:master' into master 2023-03-13 20:49:56 -06:00
MBucari ec7dd1b54a Use new AudibleApi methods 2023-03-13 20:47:32 -06:00
Robert McRackan bb900b31ef update dependencies 2023-03-13 22:28:46 -04:00
MBucari eed42bd108 Improve grid update performance 2023-03-11 21:50:30 -07:00
MBucari 3f0e6b9ee5 Fix window restore maximize statate on secondary monitor. 2023-03-11 21:35:33 -07:00
MBucari 5ec01913d5 Fix bug where book with corrupt image cannot be queued. 2023-03-11 20:58:06 -07:00
rmcrackan 245e55782e Merge pull request #527 from Mbucari/master
Improve Library Display performance and Refactor grid viewmodels
2023-03-11 16:44:31 -05:00
MBucari cc306e0e19 Fix expand/collapse button icon in Avalonia 2023-03-11 12:28:34 -07:00
MBucari 26a9bc6bbf Merge branch 'master' of https://github.com/Mbucari/Libation 2023-03-11 11:12:05 -07:00
MBucari fb9d062545 WinForms and Avalonia now share all GridEntry view models 2023-03-11 11:10:58 -07:00
MBucari 49c6b391fd WinForms and Avalonia now share all GridEntry view models 2023-03-10 20:00:25 -07:00
MBucari e1cd8b8f94 Improve Library load and refresh performance 2023-03-10 19:01:49 -07:00
Robert McRackan ef1edf1136 AYCL bug fix: US and Italy 2023-03-10 15:37:51 -05:00
rmcrackan 0def1b426a Merge pull request #526 from Mbucari/master
Add better AYCL detection and add verbose library scan logging
2023-03-10 15:26:41 -05:00
Mbucari 230e014bb1 Add better AYCL detection and add verbose library scan logging 2023-03-10 13:09:59 -07:00
Robert McRackan 34f56d2fd7 Merge branch 'master' of https://github.com/rmcrackan/Libation 2023-03-08 14:07:51 -05:00
Robert McRackan c45ffaf4a6 Incr ver 2023-03-08 14:07:47 -05:00
rmcrackan ae43ab103e Merge pull request #524 from Mbucari/master
Improve library scan speed and Track and display book availability
2023-03-08 14:06:45 -05:00
Mbucari 559977ce0b Add 'Unavailable' book and pdf counts. 2023-03-08 11:26:07 -07:00
Mbucari ccd4d3e26d Check for null Plan array 2023-03-08 11:21:47 -07:00
MBucari e76f99ff28 Fix rmcrackan/Libation#523 2023-03-07 22:34:36 -07:00
MBucari d3607583ab Tweak episode scan 2023-03-07 20:32:50 -07:00
MBucari 3ebd4ce243 Show AbsentFromLastScan book status in grid 2023-03-07 20:02:22 -07:00
Mbucari f6dcc0db1d Add AbsentFromLastScan 2023-03-07 18:58:18 -07:00
MBucari bd49db83e4 Improve library scan performance 2023-03-07 15:30:22 -07:00
Mbucari 4140722a6d Merge branch 'master' of https://github.com/Mbucari/Libation 2023-03-06 16:56:57 -07:00
Mbucari da36f9414d Improve library scan performance 2023-03-06 16:49:52 -07:00
Mbucari 1510f71ca6 Merge branch 'rmcrackan:master' into master 2023-03-03 16:33:36 -07:00
Mbucari cdb27ef712 Add last downloaded info to exports 2023-03-03 15:06:06 -07:00
Robert McRackan 790319ed98 incr ver 2023-03-03 15:58:05 -05:00
rmcrackan 1b0fb2b316 Merge pull request #522 from Mbucari/master
Resolved Several Issues (It's not as bad as  2,453 lines suggests)
2023-03-03 15:56:59 -05:00
Mbucari 02371f2221 Deleting folders with custom icons no longer triggers system file warning 2023-03-03 10:56:31 -07:00
Mbucari 2b672f86be Fatten up the chevrons 2023-03-02 19:57:43 -07:00
Mbucari 36176bff33 Update ImageSharp 2023-03-02 19:41:59 -07:00
Mbucari 174b0c26b8 Update fileicon to latest version 2023-03-02 19:40:38 -07:00
Mbucari 26c60e8e79 Convert queue expand/collapse button text to images (rmcrackan/Libation#339) 2023-03-02 19:23:03 -07:00
Mbucari d94759d868 Add Last Download column to grid (rmcrackan/Libation#498) 2023-03-02 18:52:45 -07:00
Mbucari bd7e45ca3c Add last download into to database 2023-03-02 15:09:10 -07:00
Mbucari 52a863c62a Add audiobook Trash Bin 2023-03-02 13:12:32 -07:00
Mbucari fe55b90ee3 Fix rmcrackan/Libation#511 2023-03-01 22:14:57 -07:00
Mbucari df224cc7f3 Move TrackedQueue to LibationUiBase 2023-03-01 09:33:17 -07:00
Mbucari 2a59329350 Merge branch 'rmcrackan:master' into master 2023-02-28 16:41:14 -07:00
Mbucari abdf0e7261 Parallelize post-liberation tasks 2023-02-28 16:40:53 -07:00
Mbucari b9c2a1cce3 Add folder icon support to MacOS 2023-02-28 15:57:27 -07:00
rmcrackan aa86fca08f Update InstallOnMac.md 2023-02-28 15:46:51 -05:00
Robert McRackan cf9ec9facf did last tag incorrect. New version 2023-02-28 10:13:26 -05:00
Robert McRackan f6084ef10c v9.4.1 2023-02-28 10:04:47 -05:00
rmcrackan 740b73beb7 Merge pull request #518 from Mbucari/master
Improve Audible login and Libation Upgrade
2023-02-28 09:51:08 -05:00
Mbucari 5c45802391 Fixed review comments 2023-02-28 07:42:26 -07:00
MBucari 429aa603f5 Update workflows 2023-02-27 21:41:59 -07:00
MBucari 80ea394934 Merge branch 'master' of https://github.com/Mbucari/Libation 2023-02-27 16:33:16 -07:00
Mbucari bce4437c79 Change workflows 2023-02-27 16:18:48 -07:00
Mbucari b6ad1a289b Remove windows desktop runtime dependency from chardonnay 2023-02-27 16:13:40 -07:00
Mbucari 2a22d05f37 Remove windows desktop runtime dependency from chardonnay 2023-02-27 15:08:54 -07:00
Mbucari d787843fd2 Unify upgrade process and add update progress bar 2023-02-27 14:08:15 -07:00
Mbucari ded58f687d Update 2FA and Captcha controls 2023-02-27 14:08:14 -07:00
Mbucari 1f1f34b6ce Merge branch 'rmcrackan:master' into master 2023-02-27 09:36:53 -07:00
Mbucari ffadf90f4f Fix MFA and 2FA 2023-02-27 09:36:19 -07:00
rmcrackan 67807efacf Merge pull request #515 from Mbucari/patch-4
Update InstallOnMac.md
2023-02-26 15:29:55 -05:00
Mbucari 980f5afa54 Update InstallOnMac.md 2023-02-25 19:42:45 -07:00
Robert McRackan b2f68760b2 New audible api login 2023-02-24 15:52:14 -05:00
rmcrackan faf86711a5 Merge pull request #509 from Mbucari/master
Add More MP3 Options and improved AAXClean
2023-02-24 15:35:38 -05:00
Mbucari 4a78b9d28f Revert workflow change 2023-02-24 12:38:29 -07:00
Michael Bucari-Tovo 1b0a7f5062 New mp3 options and improved encoding performance 2023-02-24 12:12:41 -07:00
Mbucari 49982043e0 Merge branch 'rmcrackan:master' into master 2023-02-24 11:15:14 -07:00
Robert McRackan 378cf7057e updated to AudibleApi v8 2023-02-24 13:12:18 -05:00
Mbucari abdc0f018e Update build-linux.yml 2023-02-22 09:23:15 -07:00
Robert McRackan c65f61b92e Fix paypal links 2023-02-22 07:33:58 -05:00
Robert McRackan c12805c8ce incr ver for release 2023-02-19 14:55:55 -05:00
rmcrackan 67f9a6db78 Merge pull request #503 from Mbucari/master
Mac and Linux Arm64 releases and Fixed #502
2023-02-19 14:52:09 -05:00
Mbucari bb6336ce2a Update .releaseindex.json 2023-02-19 11:27:23 -07:00
Michael Bucari-Tovo af7a4a6acf Add comments 2023-02-19 11:11:28 -07:00
Michael Bucari-Tovo 21d18aa565 Final edits 2023-02-19 10:59:42 -07:00
Michael Bucari-Tovo c96875ba5d Add '-chardonnay' to build assets name 2023-02-19 10:23:49 -07:00
Michael Bucari-Tovo 6ebbfb8e59 Refactor SetReleaseIdentifier() 2023-02-19 10:20:01 -07:00
Michael Bucari-Tovo 1e6e28cd57 Start downloading asynchronously 2023-02-18 22:38:26 -07:00
Michael Bucari-Tovo defed72862 Force garbage collection after completing a Processable 2023-02-18 22:16:46 -07:00
Michael Bucari-Tovo 71503b34b5 Fix macOS crash 2023-02-18 20:29:10 -07:00
Michael Bucari-Tovo a00849fb6f Refactor InteropFactory 2023-02-18 13:57:00 -07:00
Michael Bucari-Tovo 14b63c0883 Add apple UUTYPEs 2023-02-18 10:27:37 -07:00
Michael Bucari-Tovo 59d556733e Edit Mac Build Script 2023-02-17 23:46:28 -07:00
Michael Bucari-Tovo a99a175683 Update AAXClean to fix #502 2023-02-17 23:20:35 -07:00
Michael Bucari-Tovo 26fedcfb60 Fix DirectorySelectControl not displaying known dir 2023-02-17 22:58:24 -07:00
Michael Bucari-Tovo dde8024506 More thread safety to address #492 2023-02-17 22:57:43 -07:00
Michael Bucari-Tovo 25f7c29380 New linux build workflows 2023-02-17 18:04:34 -07:00
Robert McRackan 2f347e83e8 fix linux 'can update'. upgrade aaxclean 2023-02-16 07:57:36 -05:00
rmcrackan 080a74884d Update InstallOnMac.md
new mac setup video
2023-02-16 07:44:09 -05:00
Robert McRackan 2dbeb64c38 incr ver. updates for mac and linux 2023-02-15 08:38:13 -05:00
rmcrackan bb508c0718 Merge pull request #489 from Mbucari/master
Mac App Bundle and added mp3 conversion support on mac
2023-02-15 08:33:06 -05:00
Michael Bucari-Tovo 9a450b0d63 add 'macOS' to mac bundle name 2023-02-15 06:31:09 -07:00
Michael Bucari-Tovo c1de0e60d2 Hopefully fix #492 2023-02-14 23:07:40 -07:00
Mbucari dc7c03661d Add auto update to linux and macos 2023-02-14 23:06:14 -07:00
Mbucari 952eee6d32 Merge branch 'rmcrackan:master' into master 2023-02-13 21:42:11 -07:00
Michael Bucari-Tovo 472a0f30b9 Launch hangover from Libation app bundle for mac 2023-02-13 21:40:53 -07:00
Robert McRackan 73533c58a8 update dependencies 2023-02-13 21:14:56 -05:00
Mbucari 65ef018719 Move NameListFormatter to its own class 2023-02-13 10:09:13 -07:00
Mbucari f0ca349539 Update UNSAFE_MigrationHelper with new appsettings.json getter 2023-02-13 09:03:03 -07:00
Mbucari 500b287721 Fix #490 2023-02-13 08:08:10 -07:00
Mbucari 21f3ae45d3 Delete deb.yml 2023-02-12 22:25:39 -07:00
Michael Bucari-Tovo d496564f0d Edit Mac and Linux bundle build workflows 2023-02-12 21:50:33 -07:00
Michael Bucari-Tovo 6fdd6293ce Ensure appsettings.json is created in a writable location. 2023-02-12 15:32:51 -07:00
Michael Bucari-Tovo 3bca495521 Add MacOS app bundle workflow 2023-02-11 23:38:17 -07:00
Michael Bucari-Tovo 0fb580f1a5 Ensure appsettings.json is created in a writable location. 2023-02-11 20:06:04 -07:00
Michael Bucari-Tovo a7cd47e0b1 Update AAXClean 2023-02-11 18:34:07 -07:00
Robert McRackan 30aecedfae incr ver 2023-02-10 23:16:22 -05:00
rmcrackan e72799efe5 Merge pull request #487 from Mbucari/master
Custom author and narrator names formatting and batch locate books
2023-02-10 23:14:31 -05:00
Michael Bucari-Tovo ee8c0ae27b Use new .NET regular expression source generators 2023-02-10 19:45:52 -07:00
Mbucari 5b4a4341ad More agressive garbage collection 2023-02-10 15:03:43 -07:00
Mbucari 56823c1105 Move FindAudiobooks() to AudioFileStorage 2023-02-10 14:54:29 -07:00
Mbucari 1f4ada604a Make suggested changes 2023-02-10 14:37:28 -07:00
Mbucari 3a4ab80892 Add human name parsing and formatting to naming templates 2023-02-10 12:53:12 -07:00
Mbucari bba9c2ba7b Add Locate Audiobooks function (#485) 2023-02-10 09:35:21 -07:00
Robert McRackan c4acd5d208 incr ver 2023-02-08 13:56:13 -05:00
rmcrackan 381440db4c Merge pull request #479 from Mbucari/master
Fix #478 and other stuff I'd already worked on
2023-02-08 13:43:56 -05:00
Michael Bucari-Tovo 00c8be1f7e Create LibationUiBase for shared UI code 2023-02-08 09:30:13 -07:00
rmcrackan d665122aa2 Update GettingStarted.md
Classic vs Chardonnay
2023-02-08 07:49:18 -05:00
Michael Bucari-Tovo bb40df5fa3 Fix #478 2023-02-07 22:58:29 -07:00
Mbucari e3c9f70dff Move shared GUI code into AppScaffolding 2023-02-06 16:04:58 -07:00
Mbucari b351033cec Improve download and convert speed estimate 2023-02-06 15:54:12 -07:00
Mbucari 18f69bc73d Refactor Naming Template 2023-02-06 15:24:18 -07:00
Robert McRackan 39fe7b79d2 Bug fix #474 2023-02-06 08:35:24 -05:00
rmcrackan 85769d797b Merge pull request #473 from Mbucari/master
Made changes discussed in previous PR and fixed #472
2023-02-04 17:32:53 -05:00
Michael Bucari-Tovo 9a80f18e1c Rename ConditionalTagClass to ConditionalTagCollection 2023-02-04 15:19:47 -07:00
Michael Bucari-Tovo aec8305e52 Fix #472 2023-02-04 12:49:59 -07:00
Michael Bucari-Tovo a672174a9b Refactor Naming Templates 2023-02-04 12:49:48 -07:00
Mbucari 6f490b4491 Rename TagClass to TagCollection 2023-02-03 17:14:04 -07:00
Mbucari 5917d059e4 Add Enumerable initializers 2023-02-03 17:03:15 -07:00
Robert McRackan 40602c7626 incr ver 2023-02-03 17:26:17 -05:00
rmcrackan 7d5ee2afa8 Merge pull request #471 from Mbucari/master
Add a more general NamingTemplate
2023-02-03 17:24:37 -05:00
Mbucari 08b6f8fa11 Add test 2023-02-03 14:46:48 -07:00
Mbucari 5f9699aa3b Merge branch 'master' of https://github.com/Mbucari/Libation 2023-02-03 14:43:42 -07:00
Mbucari 70607aaaf4 Documentation 2023-02-03 14:43:35 -07:00
Mbucari 1d96d39af7 Add Naming Template Documentation 2023-02-03 14:33:11 -07:00
Mbucari 5557772957 Add conditional negation 2023-02-03 11:47:55 -07:00
Mbucari 5c7db6cd23 Add <series#> tag zero padding (#466) 2023-02-03 11:31:15 -07:00
Mbucari c72b64d74c Properly truncate filenames 2023-02-03 09:53:40 -07:00
Mbucari 20474e0b3c Add a more general NamingTemplate 2023-02-02 22:48:27 -07:00
Robert McRackan 867085600c New feature #469 - <language> and <language short> template options 2023-02-01 12:12:50 -05:00
rmcrackan 74290ec609 Merge pull request #468 from rmcrackan/dependabot/github_actions/docker/build-push-action-4
Bump docker/build-push-action from 3 to 4
2023-01-31 10:18:40 -05:00
dependabot[bot] 5ee555e60c Bump docker/build-push-action from 3 to 4
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-31 14:11:36 +00:00
Robert McRackan a36c28d48f incr. ver. 2023-01-30 21:24:54 -05:00
rmcrackan 0877f2c042 Merge pull request #464 from Mbucari/master
Fix #463
2023-01-25 20:48:58 -05:00
Mbucari 2baf5243ea Fix #463 2023-01-25 14:23:02 -07:00
rmcrackan b7e71f5812 Merge pull request #462 from Mbucari/master
Refactor AaxDecrypter
2023-01-25 07:06:37 -05:00
Michael Bucari-Tovo 2ed1076fab Cleanup 2023-01-24 23:35:05 -07:00
Michael Bucari-Tovo 0b20aa751f Only Dispose of NFS on disposing 2023-01-24 23:00:07 -07:00
Michael Bucari-Tovo 05a4ece8d1 Merged 2023-01-24 22:27:31 -07:00
Michael Bucari-Tovo 25b37c6266 Refactor AaxDecrypter 2023-01-24 22:25:55 -07:00
Robert McRackan b668cff0ac Update dependenciesd. Build is broken until the ambiguous ref.s moved into Dinah.Core are resolved 2023-01-24 22:40:08 -05:00
Robert McRackan 4d6c742ae9 Bug fix #459 , New feature/setting #366 2023-01-23 22:50:37 -05:00
rmcrackan 933f663d22 Merge pull request #460 from Mbucari/master
Upgrade AAXClean.Codecs to 0.5.12, add moov relocation, and fix #459
2023-01-23 22:46:20 -05:00
Michael Bucari-Tovo 0c55f278a4 Revert Solution Changes 2023-01-23 20:32:27 -07:00
Michael Bucari-Tovo 3f567ee82e Merged 2023-01-23 20:13:19 -07:00
Michael Bucari-Tovo 8dc912c11d Add option to move the moov atom to the beginning of the file. 2023-01-23 20:11:00 -07:00
Michael Bucari-Tovo f1b4e2a17d Upgrade AAXClean.Codecs to 0.5.11 2023-01-23 19:04:15 -07:00
Michael Bucari-Tovo 630cfdeab3 Upgrade AAXClean.Codecs to 0.5.11 2023-01-23 17:39:08 -07:00
Michael Bucari-Tovo 7029409792 Upgrade AAXClean.Codecs to 0.5.10 and fix #459 2023-01-23 16:30:17 -07:00
rmcrackan d0727b5a85 Update InstallOnMac.md
Added @Mbucari 's awesome video
2023-01-23 08:17:28 -05:00
rmcrackan 9f52ad5e0a Update README.md
link to docker readme
2023-01-23 08:07:37 -05:00
rmcrackan 501ae643f7 Merge pull request #458 from pixil98/master
Deb build tweaks, Docker read me
2023-01-23 08:06:51 -05:00
Aaron Reisman 400074170e Add docker readme 2023-01-22 17:09:25 -06:00
Aaron Reisman 17103ed066 Get release id correctly 2023-01-22 15:33:27 -06:00
Aaron Reisman b6b29309c9 Go back to the old way of uploading assets 2023-01-22 15:15:56 -06:00
Aaron Reisman a04538710f Try with a different glob 2023-01-22 14:53:05 -06:00
Aaron Reisman 01f6f5c137 Add name to release 2023-01-22 14:17:08 -06:00
Aaron Reisman b1a37cbd8c Switch to a still maintained release action 2023-01-22 13:56:36 -06:00
Aaron Reisman 8c59e1280b Wait for deb to be finished before releasing 2023-01-22 13:20:59 -06:00
Aaron Reisman 00339127aa reference correct deb yaml 2023-01-22 13:15:51 -06:00
Aaron Reisman 5935b40b60 Remove output version 2023-01-22 13:03:23 -06:00
Aaron Reisman 6cfd2dea96 Move deb building out of the build pipeline 2023-01-22 13:01:12 -06:00
rmcrackan 7d3a39c693 Merge pull request #456 from Mbucari/master
Add file creation DateTime to naming templates
2023-01-20 15:18:11 -05:00
Mbucari 6e7a4ea475 Update date format regex and tests 2023-01-20 10:03:55 -07:00
Michael Bucari-Tovo 3479dbc3f0 Date format naming templates 2023-01-20 01:00:22 -07:00
Mbucari 9309aea6d9 Add file creation DateTime to naming templates
typo
2023-01-19 17:12:28 -07:00
Robert McRackan f72551fa9a incr ver 2023-01-19 08:06:18 -05:00
rmcrackan e3b237b75f Merge pull request #454 from Mbucari/master
Fixed #453
2023-01-18 22:23:55 -05:00
Mbucari b1ddf18f73 Merge branch 'rmcrackan:master' into master 2023-01-18 15:47:18 -07:00
Michael Bucari-Tovo 13f522abb8 Fix file extension detection error (#453) 2023-01-18 15:47:05 -07:00
Robert McRackan 3c3d956bf3 remove linux and mac from beta 2023-01-16 13:16:49 -05:00
Robert McRackan 8160547c11 incr ver 2023-01-16 13:06:19 -05:00
rmcrackan ef71d36dee Merge pull request #451 from Mbucari/patch-3
Updated for new deb package builds
2023-01-16 12:59:23 -05:00
Mbucari b0d8434455 Updated for new deb package builds 2023-01-16 10:41:21 -07:00
rmcrackan 9be0d58461 Merge pull request #450 from Mbucari/master
Update workflows and AAXClean
2023-01-16 10:38:18 -05:00
Michael Bucari-Tovo 1addcc8211 Update AAXClean and add better error handling 2023-01-15 21:42:03 -07:00
Mbucari 38c75dc8c5 Update workflows 2023-01-12 15:47:01 -07:00
rmcrackan 89c3ea8311 Merge pull request #444 from Mbucari/master
Build and attach deb package
2023-01-12 08:45:14 -05:00
Michael Bucari-Tovo 18ff799fb1 Update build scripts 2023-01-11 21:14:26 -07:00
Mbucari 67b6aaed99 Fix #447 2023-01-11 16:08:01 -07:00
Mbucari 08bb463560 Invoke observables directly instead of through event handler 2023-01-11 14:38:12 -07:00
Mbucari 97767dcabb GLOB pattern 2023-01-11 14:37:12 -07:00
Mbucari fb18940a5c Use DataGridMyRatingColumn for both user and product ratings. 2023-01-11 14:36:43 -07:00
Mbucari b823f5fa00 Import average rating 2023-01-11 14:35:14 -07:00
Mbucari d64fb081a0 Build and attach deb package 2023-01-11 14:15:07 -07:00
Robert McRackan 09118b1ddf nvm. I guess this has been default : true for a long time. Reverting 2023-01-10 10:13:47 -05:00
Robert McRackan de20590fd5 do not enable AutoScan by default 2023-01-10 10:04:53 -05:00
rmcrackan 1050ffdb24 Merge pull request #443 from Mbucari/master
Chardonnay Bugfix + Moved Default Settings into Configuration
2023-01-10 09:54:49 -05:00
Michael Bucari-Tovo 2e49c7f697 Commit edits before refresh 2023-01-09 18:57:16 -07:00
Mbucari 708cdcc24c Merge branch 'master' of https://github.com/Mbucari/Libation 2023-01-09 16:30:59 -07:00
Mbucari c89eafd568 Fix rating edits updating search results. 2023-01-09 16:27:19 -07:00
Michael Bucari-Tovo 10de241d53 Cache default values 2023-01-09 16:11:30 -07:00
Mbucari e58952035f Spaces 2023-01-09 16:06:37 -07:00
Mbucari 50a8c7508a Cache default values 2023-01-09 16:05:55 -07:00
Michael Bucari-Tovo 2b243a6934 Remove testing code 2023-01-09 15:28:36 -07:00
Michael Bucari-Tovo ece93cb4d7 Finish migrating default Configuration values into Configuration 2023-01-09 15:26:06 -07:00
rmcrackan 5b3ca0ed32 Merge pull request #442 from wtanksleyjr/master
Add a version parameter to DEB creation, do sanity checks, use it.
2023-01-09 16:36:48 -05:00
Robert McRackan 7474f1221a bug fix #441 -- new code with empty setting 2023-01-09 16:30:12 -05:00
William Tanksley d023a943c1 Add a version parameter to DEB creation, do sanity checks, use it. 2023-01-09 13:12:12 -08:00
Michael Bucari-Tovo 4e80af5c53 Bind MyRatingCellEditor background color to the cell's background 2023-01-09 14:06:06 -07:00
Michael Bucari-Tovo eee785377f Add default values to Configuration 2023-01-09 14:05:33 -07:00
Robert McRackan 915906e6ed test push ver 2023-01-09 11:59:24 -05:00
Robert McRackan 358c8b577e increment version 2023-01-09 11:53:44 -05:00
rmcrackan 5c450a01a4 Merge pull request #440 from Mbucari/master
Configuration Change Tracking and Bookk Records
2023-01-09 11:41:34 -05:00
Michael Bucari-Tovo 36264c6c6e Add back a check that was removed for testing 2023-01-08 13:19:21 -07:00
Michael Bucari-Tovo fca946bf15 Fix bug with long folder templates 2023-01-08 10:01:30 -07:00
Michael Bucari-Tovo 452ceef285 Tweak 2023-01-07 23:27:28 -07:00
Michael Bucari-Tovo 7fafee804d Double clicking on template item adds it to the template. 2023-01-07 23:15:38 -07:00
Michael Bucari-Tovo 3a48479435 Typos and formatting 2023-01-07 18:41:34 -07:00
Michael Bucari-Tovo cab8555ab5 Make UpgradeNotificationDialog a DialogWindow 2023-01-07 18:18:14 -07:00
Michael Bucari-Tovo e3b7cbcc2a Add proper Upgrade form 2023-01-07 18:09:37 -07:00
Michael Bucari-Tovo ed15614288 Improve character display in EditTemplateDialog 2023-01-07 14:33:49 -07:00
Michael Bucari-Tovo acb6d1b335 Use new Inline controls to selectively style TextBlock text 2023-01-07 12:05:38 -07:00
Michael Bucari-Tovo fe804796ab Use ContinueWith to set Rating changed value 2023-01-06 23:46:06 -07:00
Michael Bucari-Tovo 4725fe36d1 Add property changed filtering events to Configuration 2023-01-06 22:56:00 -07:00
Michael Bucari-Tovo 5c73beff4b Add PropertyChanged detection for Dictionary type settings 2023-01-06 19:46:55 -07:00
Michael Bucari-Tovo 1f7000c2c9 Add Configurations property change notifications 2023-01-06 16:50:20 -07:00
Mbucari f09baa1318 Merge branch 'rmcrackan:master' into master 2023-01-05 23:41:27 -07:00
Michael Bucari-Tovo 7eaa03e43c Add clip and bookmark viewer and exporter 2023-01-05 23:40:39 -07:00
Robert McRackan 26099303fa update dependencies 2023-01-05 21:44:34 -05:00
Michael Bucari-Tovo 6417aee780 Add book records dialog 2023-01-05 17:02:39 -07:00
rmcrackan f9deaba4c5 Merge pull request #438 from Mbucari/master
Linux and mac upgrade notification
2023-01-03 21:26:04 -05:00
Michael Bucari-Tovo ddd6a3b279 Change args 2023-01-03 15:17:57 -07:00
Michael Bucari-Tovo 9359950666 Formatting 2023-01-03 15:00:29 -07:00
Michael Bucari-Tovo d31b2a1b65 Merge branch 'master' of https://github.com/Mbucari/Libation 2023-01-03 14:56:09 -07:00
Michael Bucari-Tovo b89b4e0af4 Linux and mac upgrade notification 2023-01-03 14:55:58 -07:00
rmcrackan cbcde027b3 Merge pull request #436 from Mbucari/master
Add download speed limit
2023-01-02 13:07:03 -05:00
Mbucari d306e6bd22 Merge branch 'rmcrackan:master' into master 2023-01-02 02:54:09 -07:00
Michael Bucari-Tovo 9ec877999e Add download speed limit 2023-01-02 02:46:46 -07:00
rmcrackan f4189bf409 Merge pull request #432 from Mbucari/master
Add ability for users to edit book ratings from the main grid
2023-01-01 22:34:06 -05:00
Michael Bucari-Tovo 0ed5062683 Cancel rating edit on escape 2023-01-01 11:23:22 -07:00
Michael Bucari-Tovo 7ef666dc91 Add TCOM tag for narrator 2022-12-31 23:31:24 -07:00
Michael Bucari-Tovo 1ac825919a Remove old migrations 2022-12-31 22:41:11 -07:00
Michael Bucari-Tovo a7bf30954d Fix null file bug and add context menu to my ratings column 2022-12-31 21:09:30 -07:00
Michael Bucari-Tovo 613cfdd903 Automatic refiltering now works on chardonnay 2022-12-31 19:50:49 -07:00
Michael Bucari-Tovo 28802c8279 Refilter on search update 2022-12-31 18:41:55 -07:00
Michael Bucari-Tovo 6d7b3bd5f0 Improve star display on classic 2022-12-31 15:39:46 -07:00
Michael Bucari-Tovo b97d8e9403 Add ratings cell tool tips 2022-12-31 11:12:04 -07:00
Michael Bucari-Tovo b4838d364e Only show hollow stars in editing mode 2022-12-31 10:33:18 -07:00
Michael Bucari-Tovo 05ac5c63e1 Formatting 2022-12-31 10:16:54 -07:00
Michael Bucari-Tovo 874bf9e7c0 Improve classic and chardonnay rating editor simmilarity 2022-12-31 10:02:30 -07:00
Michael Bucari-Tovo c9497ef39e Make my rating column sortable 2022-12-30 19:44:16 -07:00
Michael Bucari-Tovo 496830d01d Fix cell editor to work with desktop scaling 2022-12-30 19:25:11 -07:00
Mbucari ccebcdd4c7 Merge branch 'rmcrackan:master' into master 2022-12-30 17:01:22 -07:00
Michael Bucari-Tovo c900fe8461 Add user rating editing to grid 2022-12-30 17:00:40 -07:00
rmcrackan a0158db37e Merge pull request #431 from Mbucari/master
Fix file naming template on unix systems (#430)
2022-12-30 11:05:09 -05:00
Robert McRackan b8c26b01ad update dependencies 2022-12-30 09:58:51 -05:00
Mbucari 3a44bef0d9 Lowercase command is more linux-ey 2022-12-29 16:24:41 -07:00
Mbucari 57a4ee781b .deb package build script (#390) 2022-12-29 16:04:20 -07:00
= e12f475850 Refactor 2022-12-29 15:39:48 -07:00
= f822a23daa Linux and OSX directory length limits 2022-12-29 15:29:18 -07:00
= 6901b8be35 Fix file naming template on unix systems 2022-12-29 14:12:46 -07:00
Robert McRackan 83fb2cd1d0 New feature #430 : bulk set pdf-downloaded status 2022-12-29 09:33:32 -05:00
Robert McRackan c98664d584 Bugfix #423 : Chardonnay updater fails when windows username has a space 2022-12-22 09:45:32 -05:00
Robert McRackan d098be8b03 EF: move seed data into corresponding config class 2022-12-22 09:37:21 -05:00
Robert McRackan 3f6689d032 Test release before v9 2022-12-20 12:43:04 -05:00
rmcrackan b4206fc203 Merge pull request #416 from pixil98/master
Linux runners and Docker image
2022-12-19 21:58:38 -05:00
rmcrackan cfa4a0c07f Merge pull request #417 from Mbucari/master
Improve download cancellation
2022-12-19 13:42:24 -05:00
Michael Bucari-Tovo 357b220ace Suppress warnings 2022-12-19 09:48:18 -07:00
Michael Bucari-Tovo 47968304c9 Return Download to new background thread 2022-12-19 09:15:36 -07:00
Michael Bucari-Tovo 2024d5e116 Improve download cancellation. 2022-12-18 21:52:51 -07:00
pixil98 5ae2a99c14 Docker workflow (#7)
* Refactored workflows
* Added docker build to release
* Linux and MacOS now build on Linux
2022-12-18 17:57:37 -06:00
rmcrackan 7fd002d2c9 Merge pull request #413 from Mbucari/master
Update obsolete code and fix #347
2022-12-18 09:41:31 -05:00
Michael Bucari-Tovo b7b7038244 Delete partially decrypted files from previous Libation instances 2022-12-17 12:30:26 -07:00
Michael Bucari-Tovo b5519c4875 Add option for user to choose custom temp folder 2022-12-17 12:25:13 -07:00
Michael Bucari-Tovo 44feab9eb2 Update comments 2022-12-17 11:39:37 -07:00
Michael Bucari-Tovo 96c45c33e5 Refactor NetworkFileStream replace obsolete WebRequest 2022-12-17 11:31:51 -07:00
Michael Bucari-Tovo 36efbcb812 Replace deprecated file dialogs 2022-12-16 21:08:11 -07:00
Michael Bucari-Tovo 03f44b4e9c Fix IDE class grouping 2022-12-16 19:54:31 -07:00
rmcrackan 19860e9f09 Merge pull request #412 from Mbucari/master
Fix Character Replacements and Add More Useful Error Messages
2022-12-16 21:28:36 -05:00
Michael Bucari-Tovo 0701cb3970 Reorder tabs 2022-12-16 16:45:51 -07:00
Michael Bucari-Tovo 7d6000e3b6 Bring Hangover Chardonnay into feature parity with Classic (#409) 2022-12-16 16:41:24 -07:00
Mbucari ef973ac56a Merge branch 'rmcrackan:master' into master 2022-12-16 08:55:08 -07:00
Robert McRackan 91a1033c52 makes slashes more clear 2022-12-16 09:48:00 -05:00
Robert McRackan 4197db6af9 Fix unit tests failing because of windows newlines 2022-12-16 09:27:44 -05:00
MBucari 210ab065c2 Make tests xplat 2022-12-15 23:04:27 -07:00
Mbucari 9cd10eca58 Merge branch 'rmcrackan:master' into master 2022-12-15 19:25:27 -07:00
Robert McRackan ba676be46d update dependency 2022-12-15 21:06:26 -05:00
Mbucari 665a2e1866 Merge branch 'rmcrackan:master' into master 2022-12-15 16:22:43 -07:00
Michael Bucari-Tovo 94469cae3d Add better error messages for license denial #352 2022-12-15 16:22:25 -07:00
Michael Bucari-Tovo a0dd2ccad6 Make filename character replacement more xplat and allow replacing any char, not just illegal. 2022-12-15 15:50:48 -07:00
Robert McRackan b2cf837de7 Hangover. WinForms. Restore deleted books 2022-12-15 14:11:27 -05:00
Mbucari 80bcf60b5b Merge branch 'rmcrackan:master' into master 2022-12-14 15:40:42 -07:00
Robert McRackan 7ad0ab566a New feature: 'Remove' now removes forever. Removed books won't be re-added on next scan 2022-12-14 16:19:55 -05:00
Michael Bucari-Tovo 2b16e86c7b Fix character replacement for non-windows platforms. 2022-12-13 16:33:37 -07:00
Robert McRackan f2ea02ae0b bugfix. file extension 2022-12-13 16:09:24 -05:00
Robert McRackan f65cd39040 bug fix: keyboard shortcuts 2022-12-13 15:58:54 -05:00
Robert McRackan 5ca0d2a399 New feature #406 : Right Click Menu for Stop-Light Icon (Chardonnay UI) 2022-12-13 15:32:33 -05:00
Robert McRackan d1528a095b New feature #406 : Right Click Menu for Stop-Light Icon (Classic UI) 2022-12-13 13:48:28 -05:00
rmcrackan 749173a463 Merge pull request #407 from Mbucari/master
Add dynamic context menus to products grid
2022-12-13 08:33:03 -05:00
Michael Bucari-Tovo 6fbd90a6b3 Fix hidden tag 2022-12-13 02:42:53 -07:00
Michael Bucari-Tovo f39d272e6a Make reused ContextMenu static 2022-12-12 18:23:12 -07:00
Michael Bucari-Tovo bb3854f512 Finishing touch 2022-12-12 17:21:41 -07:00
Michael Bucari-Tovo e40daecfb8 Remove old static context menu 2022-12-12 17:12:44 -07:00
Michael Bucari-Tovo 3716ab9cb5 Merged 2022-12-12 17:11:40 -07:00
Michael Bucari-Tovo 0cc6d6337a Add dynamic context menus to main grid 2022-12-12 17:10:18 -07:00
Robert McRackan ce711a36ba #398 - new feature: right-click, copy 2022-12-12 15:03:20 -05:00
rmcrackan 451af7bea9 Merge pull request #405 from Mbucari/master
Upgraded to Avalonia 11-Preview4
2022-12-12 13:18:06 -05:00
Michael Bucari-Tovo 63200592bf Ensure mandatory character replacements remain marked mandatory 2022-12-12 08:34:10 -07:00
Michael Bucari-Tovo d165dfbeb5 Fix NRE 2022-12-12 08:24:18 -07:00
Michael Bucari-Tovo eed3d84517 Add context menu 2022-12-11 19:21:49 -07:00
Michael Bucari-Tovo ba7d890966 Update hangover 2022-12-11 18:33:57 -07:00
Michael Bucari-Tovo 5140fc63d9 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-12-11 18:30:56 -07:00
Michael Bucari-Tovo 78509c07e0 Remove unused files 2022-12-11 18:30:48 -07:00
Mbucari 5084141215 Delete Source/Visual Studio 2022 directory 2022-12-11 18:23:00 -07:00
Michael Bucari-Tovo 3f2ac83474 Fix copying grid contents to clipboard 2022-12-11 17:50:15 -07:00
Michael Bucari-Tovo 58a0468728 Tidy up 2022-12-11 16:58:51 -07:00
Mbucari 8e13aa7513 Merge branch 'rmcrackan:master' into master 2022-12-11 15:47:24 -07:00
Michael Bucari-Tovo 48e2d91fc8 Implement Illegal Char Replace dialog in Avalonia 2022-12-11 15:47:04 -07:00
rmcrackan a7f119217f Merge pull request #404 from pixil98/master
Fix Auto Updating
2022-12-11 09:12:10 -05:00
pixil98 865f2261fe Fix Auto Updating (#8)
Release workflow no longer includes the parent folder in the zip.
2022-12-10 00:30:08 -06:00
Michael Bucari-Tovo dfedb23efd Refactor ProductsDisplay 2022-12-09 12:27:52 -07:00
Mbucari c01e1c3e4b Merge branch 'rmcrackan:master' into master 2022-12-09 12:27:14 -07:00
Robert McRackan ad8dac5fb0 more forgiving releaseindex windows regex 2022-12-08 08:17:17 -05:00
Mbucari 84e81b6218 Merge branch 'rmcrackan:master' into master 2022-12-07 19:13:22 -07:00
Robert McRackan 86efe631fe restore yaml 2022-12-07 13:49:05 -05:00
Robert McRackan f5f1dc483b publish debugging. create new version 2022-12-07 13:34:30 -05:00
Robert McRackan 8aa4328c6c update dependencies 2022-12-07 13:09:02 -05:00
Michael Bucari-Tovo a01a8c4b19 Update Avalonia to v.11-Preview-4 2022-12-07 10:15:12 -07:00
Robert McRackan 4b2387b621 update dependencies 2022-12-07 11:53:10 -05:00
Robert McRackan 74d16d8ef9 yaml releases don't run. comment out for now 2022-12-07 07:42:11 -05:00
rmcrackan b1ea8f9fa7 Update GettingStarted.md
disclaimer: don't install in Program Files
2022-12-06 15:59:11 -05:00
rmcrackan c666fdeaff document CLI set-status 2022-12-06 15:02:26 -05:00
Robert McRackan 7068782975 Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-12-06 14:58:30 -05:00
Robert McRackan c4cebbebe7 * #396 New feature : match download status to files
* UI: Visible Books \> Set 'Downloaded' status automatically. Visible books. Prompts before saving changes
  * CLI: Full library. No prompt
2022-12-06 14:58:22 -05:00
rmcrackan 53d43d9fa9 Merge pull request #401 from pixil98/master
Add validate and release workflows
2022-12-05 14:54:39 -05:00
Aaron Reisman 11d59beeed Rename happens before zipping 2022-11-28 13:08:57 -06:00
Aaron Reisman ef71e297f4 Add special handling for classic build 2022-11-28 12:54:17 -06:00
Aaron Reisman 1e4d1d1973 Lowercase OS names in releaseindex 2022-11-28 12:44:48 -06:00
pixil98 893d99854b Merge branch 'master' into master 2022-11-25 00:20:51 -06:00
Aaron Reisman db93980cd5 Rename publish to release 2022-11-24 23:54:49 -06:00
pixil98 34fac30b2b Merge official updates (#6)
Pull latest Libation updates, fix move to net7
2022-11-24 23:53:00 -06:00
pixil98 2fa0bcb765 Near final workflows
Updated workflows to release zips with the correct file names.
2022-11-23 15:48:37 -06:00
pixil98 78fd09aa91 Proper build
Builds all packages properly
2022-11-22 10:45:33 -06:00
Robert McRackan a54516b4f5 Fix pubxml hierarchy 2022-11-21 13:51:30 -05:00
Robert McRackan f193d6f376 AudibleApi fixed 2022-11-17 17:05:58 -05:00
pixil98andAaron Reisman 8a82c294a1 Fix publish workflow (#2)
* Add dotnet test workflow

* main -> master

* Try a different workflow

* Add working-directory

* use windows runner

* use env var

* Fix build and test order

* Specify configuration

* Specify sln instead of working dir

* Specify that DOTNET_SLN is an env var

* Add publish workflow

* Add env.DOTNET_SLN to publish workflow

* Add publish job

* Combine publish into one job

* Just create an artifact

* Remove unused nuget lines

* Add Publish job back

Co-authored-by: Aaron Reisman <areisman@epic.com>
2022-11-16 13:40:57 -06:00
Robert McRackan 9392cf4bf0 update dependencies 2022-11-16 13:36:23 -05:00
Robert McRackan ec4deb9099 update db dependencies 2022-11-16 12:48:22 -05:00
Robert McRackan cf0548aab9 update dependencies 2022-11-16 12:46:12 -05:00
pixil98 064801380b Add workflows (#1)
Adds basic workflows
2022-11-16 11:44:15 -06:00
Robert McRackan 9dc2a7424a DataLayer => .net7 2022-11-16 11:49:07 -05:00
Robert McRackan 4c8a56a5b9 Core, Utilities => .net7 2022-11-16 11:47:34 -05:00
Robert McRackan 9aad263996 Domain Internal Utilities => .net7 2022-11-16 11:46:26 -05:00
Robert McRackan ce1ab7c20d Domain Utilities => .net7 2022-11-16 11:45:04 -05:00
Robert McRackan c9217990cd applications => .net7 2022-11-16 11:43:35 -05:00
Robert McRackan 90cbf3b7a6 tests => .net7 2022-11-16 11:27:51 -05:00
Robert McRackan c4f1b22ddf demos => .net7 2022-11-16 11:24:30 -05:00
Robert McRackan fb612ea6ab Bug fix #394 : Scanning dir.s containing symlinks causes errors. Thanks @CharlieRussel 2022-11-14 16:22:29 -05:00
Robert McRackan bce44b6f6d Fix string interpolation bugs 2022-11-14 15:16:22 -05:00
Robert McRackan 7575736991 Bugfix #389 : Handle corrupt cache file 2022-11-08 07:22:08 -05:00
Robert McRackan 06f8d055fc update dependencies 2022-11-02 16:24:14 -04:00
Robert McRackan d64e043fe8 #367 : New template option "year": year published to audible 2022-10-21 13:41:44 -04:00
Robert McRackan 99564d9c25 update dependencies 2022-10-18 08:33:43 -04:00
rmcrackan 29bccd3e33 Update InstallOnMac.md
Gatekeeper instructions
2022-10-05 22:25:40 -04:00
Robert McRackan 20f65f6534 Fix description of poorly named AutoDownloadEpisodes 2022-09-28 15:49:16 -04:00
Robert McRackan 8ca72b2e2d incr ver 2022-09-28 13:27:04 -04:00
Robert McRackan 75429f288f update dependencies 2022-09-28 13:25:41 -04:00
Robert McRackan d1bb921346 Cache assembly fetches/resolution so that repeat errors aren't clogging the log 2022-09-24 09:48:44 -04:00
rmcrackan b979b6ddad Update README.md 2022-09-24 07:54:48 -04:00
rmcrackan 4eba41ddbb Update GettingStarted.md 2022-09-23 21:48:16 -04:00
rmcrackan 418f5062ff Update README.md
remove unofficial linux instructions
2022-09-23 21:46:45 -04:00
rmcrackan f736f7f909 Update GettingStarted.md
remove unofficial linux instructions
2022-09-23 21:46:06 -04:00
rmcrackan 96ead28246 Update Advanced.md
remove unofficial linux instructions
2022-09-23 21:45:28 -04:00
Robert McRackan 34bad7a53d fix string template 2022-09-19 14:08:46 -04:00
Robert McRackan 7ac1fff3a0 update dependencies 2022-09-09 11:53:59 -04:00
Robert McRackan a4c5c53df3 incr ver 2022-09-09 11:10:56 -04:00
Robert McRackan 87db5cfd94 revert accidental re-name of button text 2022-09-09 11:04:14 -04:00
Robert McRackan 85e7bbf366 Chardonnay readonly textboxes should be grey (as they are in Classic) 2022-09-07 13:29:41 -04:00
Robert McRackan c55c5fac23 typo 2022-09-06 12:47:44 -04:00
Robert McRackan e25e2f7211 Update documentaion for macos 2022-08-31 15:44:12 -04:00
Robert McRackan f310d583d8 Bug fix #364 - app was crashing on attempt to download PDF to which the user no longer had ownership. Eg: returned or Plus catalog 2022-08-29 15:05:56 -04:00
Robert McRackan f05465b29b incr ver 2022-08-18 13:38:23 -04:00
rmcrackan 959e31972e Merge pull request #363 from Mbucari/master
Change assembly loadig
2022-08-18 13:36:08 -04:00
Michael Bucari-Tovo 17181811f0 Remove assembly hot loading 2022-08-18 11:21:40 -06:00
Michael Bucari-Tovo 6d2624d52b Fix comment 2022-08-18 10:59:37 -06:00
Michael Bucari-Tovo 9dd5940c8c Remove trailing wild 2022-08-18 10:59:00 -06:00
Michael Bucari-Tovo 1927d19961 comments 2022-08-18 10:47:53 -06:00
Michael Bucari-Tovo 09cc838bb4 Checks 2022-08-18 10:45:07 -06:00
Michael Bucari-Tovo 8af4c71101 Change assembly loadig 2022-08-18 10:29:30 -06:00
Robert McRackan 7ffdf45164 Bug fix #361 : import would break when audible erroneous duplicates a name in the author list or a name in the narrator list. (Note: the same name as both author and narrator has always been ok.) 2022-08-17 20:05:47 -04:00
Robert McRackan e0999dc9ae Bug fix #358 : pdf downoad errors in CLI were crashing the rest of the loop 2022-08-16 15:41:12 -04:00
Robert McRackan a0f3d44e97 revert changes to DownloadDecryptBook. This is not the correct fix 2022-08-16 14:54:12 -04:00
Robert McRackan 1510a86579 Bug fix #350 : support old style of large multi-part books 2022-08-16 10:14:13 -04:00
Robert McRackan b3581455d2 incr ver. Don't re-use previously bad build number 2022-08-15 22:03:51 -04:00
Robert McRackan 8ee1019fa5 ConfigApp.s need PublishReadyToRun and RuntimeIdentifies 2022-08-15 22:01:38 -04:00
Robert McRackan 285b10a95f bug fix: WindowsConfigApp must explicitly load a type from the Dinah.Core.WindowsDesktop asm since avalonia doesn't reference it 2022-08-15 10:20:41 -04:00
Robert McRackan 0ca33f864b oops: recent bug fix introduced an infinite loop. fixed 2022-08-15 09:41:33 -04:00
Robert McRackan a0823fa26c Bug fix. Book details dialog save button should also close the form 2022-08-14 21:53:34 -04:00
Robert McRackan aa9040da5d Fix release pragma OS var.s 2022-08-14 19:35:01 -04:00
Robert McRackan 222031ecc5 avalonia ui: add new setting 2022-08-14 11:12:52 -04:00
Robert McRackan dda8f5a974 publish profiles should point to Publish dir 2022-08-14 09:32:50 -04:00
Robert McRackan e9b484df04 * LoadByOS build profiles
* incr ver
2022-08-14 09:19:14 -04:00
rmcrackan d505264e86 Merge pull request #356 from Mbucari/master
Add useCoverAsFolderIconCb setting to avalonia
2022-08-13 18:33:38 -04:00
Michael Bucari-Tovo c0b1f1dc0a Add useCoverAsFolderIconCb setting to avalonia 2022-08-12 18:37:02 -06:00
Robert McRackan 1524d558a4 * Feature #307 : New windows setting to use cover art as folder's icon. Incomplete. Need to add to avalonia settings
* Interop refactor
2022-08-12 17:55:15 -04:00
Robert McRackan aea8c11dc4 Add OS-specific interop 2022-08-12 13:49:51 -04:00
Robert McRackan 86c7f89788 update dependencies 2022-08-08 11:47:08 -04:00
Robert McRackan 3272541e81 Audible changed how scanning works. You must upgrade to scan again 2022-08-02 21:20:14 -04:00
Robert McRackan 3b3d40e4e6 Add classic/chardonnay to About box 2022-08-02 14:40:58 -04:00
Robert McRackan a47866b6f7 Open file/folder is now cross platform 2022-08-02 12:56:52 -04:00
Robert McRackan 0df4dfdef5 update dependencies 2022-08-02 09:14:36 -04:00
Robert McRackan fe2de6ecf7 recommended: System.Environment.ProcessPath 2022-08-02 07:58:42 -04:00
Robert McRackan fc25e73b1a attach avalonia primer notes 2022-08-01 20:56:08 -04:00
Robert McRackan a3df85c87e Refactor hangover 2022-08-01 11:59:55 -04:00
Robert McRackan 553a936e7e incr ver 2022-08-01 09:49:45 -04:00
Robert McRackan 635764625e incl HangoverAvalonia in sln 2022-08-01 07:46:44 -04:00
rmcrackan f5599f7c57 Merge pull request #338 from Mbucari/master
HangoverAvalonia and other fixes
2022-08-01 07:36:25 -04:00
Mbucari dc6aaf2dd6 Updated instructions for Standalone Build (no dotnet runtime required) 2022-07-31 22:37:42 -06:00
Michael Bucari-Tovo f1ba2b4ae8 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-31 22:24:28 -06:00
Michael Bucari-Tovo 742310b8d6 Fix install workflow 2022-07-31 22:24:17 -06:00
Mbucari 073787173d Merge branch 'rmcrackan:master' into master 2022-07-31 21:59:46 -06:00
Michael Bucari-Tovo 66679ace2f Add HangoverAvalonia 2022-07-31 20:33:56 -06:00
Robert McRackan 3982537d46 Tags no longer saved outside of database 2022-07-31 21:58:53 -04:00
Robert McRackan 7cf4c63d79 OSX-specific bug fix for search engine 2022-07-31 14:24:24 -04:00
Robert McRackan 7c4575cf66 incr ver 2022-07-30 22:39:41 -04:00
rmcrackan f4749d703f Merge pull request #337 from Mbucari/master
Fixes and Improvements
2022-07-30 22:24:37 -04:00
Michael Bucari-Tovo f2f562619b Updated dependencies 2022-07-30 20:09:17 -06:00
Robert McRackan 16c019a9c6 update dependencies 2022-07-30 21:54:10 -04:00
Robert McRackan 644dcbdd4d updated dependency 2022-07-30 21:40:31 -04:00
Michael Bucari-Tovo 6b112f5248 Delete obj and bin folders on clean 2022-07-30 18:03:33 -06:00
Michael Bucari-Tovo 0bfa609058 Libation Runs on MacOS 2022-07-30 16:09:31 -06:00
Michael Bucari-Tovo 8020ded642 Add platform preprocessor definitions 2022-07-30 13:42:11 -06:00
Michael Bucari-Tovo c4cd6b16fc Add macOS ID 2022-07-30 11:04:01 -06:00
Michael Bucari-Tovo 310012fd17 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-30 11:03:19 -06:00
Michael Bucari-Tovo 06163db6ff Merge Conflict 2022-07-30 11:03:17 -06:00
Michael Bucari-Tovo 7689eed711 Add macOS identifier 2022-07-30 10:58:53 -06:00
Mbucari d396d697d7 Fix typos 2022-07-30 10:33:04 -06:00
Michael Bucari-Tovo 27ed11d904 More universal updating 2022-07-30 09:49:11 -06:00
Michael Bucari-Tovo 9e7670b918 Fix Subdirectory being added to custom directory selection 2022-07-30 09:48:57 -06:00
Michael Bucari-Tovo 31e97defd1 Add ReleaseIdentifier to logging 2022-07-30 09:48:27 -06:00
Mbucari 1a447627c7 Merge branch 'rmcrackan:master' into master 2022-07-28 20:04:36 -06:00
Robert McRackan 962b386d07 Bug fix: update checking code 2022-07-28 21:35:59 -04:00
Michael Bucari-Tovo d69ff24c2d Modularize update process 2022-07-28 17:18:43 -06:00
Michael Bucari-Tovo 070ed1d373 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-28 17:06:18 -06:00
Michael Bucari-Tovo 47729bf7b0 fix release getter 2022-07-28 17:06:12 -06:00
Robert McRackan ed0ce2976b Bug fix #329 : Chardonnay-beta freezes after a scan 2022-07-28 13:26:16 -04:00
rmcrackan 2224f46ed5 Merge pull request #331 from Mbucari/master
Fix MessageBox hang
2022-07-28 13:22:03 -04:00
Michael Bucari-Tovo 433974323c Remove unnecessary extensions 2022-07-28 11:17:29 -06:00
Michael Bucari-Tovo 7525d318c0 Crean up helper methods 2022-07-28 11:03:22 -06:00
Michael Bucari-Tovo 92327dcc0d Add synchronous thread extensions 2022-07-28 10:40:39 -06:00
Michael Bucari-Tovo aeaf234edd Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-28 10:13:28 -06:00
Michael Bucari-Tovo a99b644917 Fix thread hang issue#329 2022-07-28 10:12:43 -06:00
Mbucari d79a55e5c9 Merge branch 'rmcrackan:master' into master 2022-07-28 09:43:20 -06:00
Mbucari 16b0feeb82 Create feature request template 2022-07-28 09:43:11 -06:00
Mbucari 7b3a25e45a Create bug report template 2022-07-28 09:42:33 -06:00
Robert McRackan 8effdcb92d add macos publish options. standardize publish profiles 2022-07-28 10:43:00 -04:00
Robert McRackan b12bef81bd These stupid unused language packs are 40% of our disk usage. And the SatelliteResourceLanguages bug *still* isn't fixed 2022-07-28 09:36:12 -04:00
Robert McRackan f04a5e0168 tweaks to getLatestRelease 2022-07-27 16:36:18 -04:00
rmcrackan e093729707 Merge pull request #325 from Mbucari/master
Add app update to Avalonia Build
2022-07-27 15:49:41 -04:00
Michael Bucari-Tovo 369151ada2 Revert timeout time 2022-07-27 09:55:09 -06:00
Michael Bucari-Tovo 1f685ae8a0 Add release index download 2022-07-27 09:49:58 -06:00
Mbucari bbe91099cb Update .releaseindex.json 2022-07-27 09:45:29 -06:00
Mbucari 92015ba4c2 Add files via upload 2022-07-27 09:27:57 -06:00
Mbucari 3bcacabadc Delete appcasttest.xml 2022-07-27 01:03:24 -06:00
Mbucari f5736d9151 Merge branch 'rmcrackan:master' into master 2022-07-27 00:46:04 -06:00
Michael Bucari-Tovo 59015f438e Add auto app update to windows avalonia 2022-07-27 00:36:13 -06:00
Michael Bucari-Tovo 3af47ab395 Add update name pattern matching 2022-07-27 00:35:55 -06:00
Michael Bucari-Tovo 308619b01a Fix bug if MessageBox called from worker thread 2022-07-27 00:30:35 -06:00
Robert McRackan 4efce57488 gitignore bin-Avalonia 2022-07-26 22:15:23 -04:00
Robert McRackan c8ee950f7d Linux beta 2022-07-26 21:14:43 -04:00
Mbucari 0bba0f9256 Add files via upload 2022-07-26 12:53:49 -06:00
rmcrackan 05bdff5123 Merge pull request #321 from Mbucari/master
Linux Beta
2022-07-26 08:16:55 -04:00
Mbucari e58e6cfb9f Update README.md 2022-07-25 19:51:35 -06:00
Mbucari b052871004 Update README.md 2022-07-25 19:48:01 -06:00
Mbucari d738f4f35f Update README.md 2022-07-25 17:33:59 -06:00
Michael Bucari-Tovo 7286aee9dd Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-25 17:22:29 -06:00
Michael Bucari-Tovo ca455978a5 Update for Linux 2022-07-25 17:22:17 -06:00
Mbucari 9c38bea5b7 Update README.md 2022-07-25 17:21:35 -06:00
Michael Bucari-Tovo fbec1bc569 Linux pub 2022-07-25 14:45:39 -06:00
Michael Bucari-Tovo 6dd885f0b2 Wrap save and restore in tyy/catch blocks 2022-07-25 08:19:46 -06:00
Mbucari ab38eb5571 Update README.md 2022-07-25 00:05:46 -06:00
Michael Bucari-Tovo 0e4b9ab396 Build standalone 2022-07-25 00:04:52 -06:00
Michael Bucari-Tovo 7dfedbc73b Remove beta checkbox 2022-07-24 16:51:38 -06:00
Michael Bucari-Tovo 625ae1d63c Removed Avalonia from LibationWinForms 2022-07-24 16:42:38 -06:00
Michael Bucari-Tovo 71098ef02f Publish Profiles 2022-07-24 16:33:45 -06:00
Mbucari d63a6de543 Update README.md 2022-07-24 16:33:19 -06:00
Mbucari 2a71a85306 Update README.md 2022-07-24 16:19:03 -06:00
Michael Bucari-Tovo 6de3a8a2bf Linux instructions 2022-07-24 16:14:34 -06:00
Michael Bucari-Tovo 3fc1da66de Linux compat 2022-07-24 14:46:27 -06:00
Michael Bucari-Tovo 683c221ca8 Linux compatability 2022-07-24 14:18:26 -06:00
Michael Bucari-Tovo fe6cfc899b Add Avalonia setup 2022-07-24 13:04:19 -06:00
Michael Bucari-Tovo ffd947eb2e A 2022-07-23 21:04:27 -06:00
Michael Bucari-Tovo 8dd59cb08a Refactor 2022-07-23 20:54:02 -06:00
Michael Bucari-Tovo 1e4c489983 Libation Runs on Linux! 2022-07-23 18:07:04 -06:00
Michael Bucari-Tovo 17b0da358f Add LinkLabel control 2022-07-22 20:11:13 -06:00
Michael Bucari-Tovo 6aa0a1f8b9 Remove references to winforms 2022-07-22 19:28:31 -06:00
Michael Bucari-Tovo ab731a63af Tweak MessageBox 2022-07-22 19:20:47 -06:00
Michael Bucari-Tovo 07d2c656fc Add description text 2022-07-22 18:33:49 -06:00
Michael Bucari-Tovo 9ecb32c3d2 Added login dialogs 2022-07-22 18:25:47 -06:00
Michael Bucari-Tovo 503e1e143e Separate invalid char check for folders and files. Files can't have slashes. 2022-07-22 18:11:39 -06:00
Mbucari e34ce67a2c Merge branch 'rmcrackan:master' into master 2022-07-22 12:39:09 -06:00
Robert McRackan a0fd0a3de6 Book details dialog. On open, tags should be first focus 2022-07-22 11:15:04 -04:00
Robert McRackan 7f3cbc454f Bug fix #319 : in some cases mp3 chapter metadata was incorrect 2022-07-21 22:29:12 -04:00
Mbucari 30eb117fa1 Merge branch 'rmcrackan:master' into master 2022-07-21 10:05:12 -06:00
Robert McRackan 63877160aa New feature #170 : book details, added link to audible's page for that book 2022-07-21 09:02:42 -04:00
Robert McRackan 77e61479cf New feature #284 : Add bitrate, sample rate, and channels to template options and to exports 2022-07-21 08:37:04 -04:00
Michael Bucari-Tovo ca71283108 Revert 2022-07-20 20:10:07 -06:00
Michael Bucari-Tovo 285563af5e Revert 2022-07-20 20:08:53 -06:00
Michael Bucari-Tovo 62cbad0d8f Commit works in progress 2022-07-20 19:41:56 -06:00
Michael Bucari-Tovo 2cb2479d63 Added EditTemplateDialog and LibationFilesDialog 2022-07-20 13:35:30 -06:00
Mbucari e7c5b1d8dc Merge branch 'rmcrackan:master' into master 2022-07-19 14:27:11 -06:00
Robert McRackan 7f086aeaac Bug fix #318: Audible changed their API, likely in conjunction with shutting down the Windows App. DownloadQuality.Extreme and DownloadQuality.Low now throw errors 2022-07-19 15:02:31 -04:00
Robert McRackan 78186d4973 update dependencies 2022-07-19 09:32:26 -04:00
Robert McRackan 4d84174ba6 AudibleApi: Make exceptions more flexible so that less logic is needed inside catch 2022-07-19 08:19:03 -04:00
Robert McRackan 579536f65a Revert: project publish => sln publish 2022-07-19 07:43:34 -04:00
Michael Bucari-Tovo a4ff739684 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-18 23:01:20 -06:00
Michael Bucari-Tovo 9e06c70319 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-18 23:00:55 -06:00
Michael Bucari-Tovo 0c98ce000b Added SettingsDialog 2022-07-18 23:00:40 -06:00
Robert McRackan 230b23dc80 on startup, log BetaOptIn 2022-07-18 22:15:11 -04:00
Robert McRackan d55b8eeeba Turn 'self contained' back on 2022-07-18 20:56:09 -04:00
rmcrackan decf75411f Merge pull request #313 from Mbucari/master
Fixed hidden items being duplicated on library scan
2022-07-18 15:08:04 -04:00
Mbucari c69f14dac5 Merge branch 'rmcrackan:master' into master 2022-07-18 12:37:13 -06:00
Michael Bucari-Tovo 10359aa5e8 Fixed hidden items being duplicated on library scan 2022-07-18 12:36:53 -06:00
Robert McRackan 72e030faaf Include beta feature: cross-platform UI 2022-07-18 14:26:19 -04:00
rmcrackan b21055d0ea Merge pull request #312 from Mbucari/master
Avalonia Beta release
2022-07-18 14:15:15 -04:00
Michael Bucari-Tovo 720fd64c97 Fixed visible books count not updating 2022-07-18 10:07:13 -06:00
Michael Bucari-Tovo e9a331292a Remove Commit method 2022-07-17 01:26:18 -06:00
Michael Bucari-Tovo 51fee4ae24 Remain classes and fix adding row to EditTagsDialog 2022-07-17 00:59:26 -06:00
Michael Bucari-Tovo 4cfe72a63b Add WheelComboBox 2022-07-17 00:44:32 -06:00
Michael Bucari-Tovo 6a8476c976 Undo change 2022-07-17 00:07:04 -06:00
Michael Bucari-Tovo 8bb17d09c3 Added beta opt-in setting 2022-07-16 23:57:12 -06:00
Michael Bucari-Tovo ad6b86fcb4 Added DescriptionDisplayDialog and ImageDisplayDialog 2022-07-16 23:27:56 -06:00
Michael Bucari-Tovo 1578be2520 Added MessageBoxAlertAdmin 2022-07-16 22:04:00 -06:00
Michael Bucari-Tovo 82d8d954ef Added EditQuickFilters dialog 2022-07-16 21:24:07 -06:00
Michael Bucari-Tovo eff9c2b35d Added AccountsDialog 2022-07-16 20:47:53 -06:00
Michael Bucari-Tovo ccdd1dc9f3 Added BookDetailsDialog, LiberatedStatusBatchDialog, ScanAccountsDialog, SearchSyntaxDialog and TagsBatchDialog 2022-07-16 17:47:54 -06:00
Michael Bucari-Tovo 952173d450 Added book details dialog 2022-07-16 15:06:37 -06:00
Michael Bucari-Tovo 35f677a0fa Added gridlines 2022-07-15 20:57:22 -06:00
Michael Bucari-Tovo 51d0645699 Add stasrt time testing 2022-07-15 17:05:13 -06:00
Michael Bucari-Tovo 0189a197a8 Refactoring 2022-07-15 16:36:58 -06:00
Michael Bucari-Tovo 1ce5fedc8c Refactor ProductDisplay 2022-07-15 15:58:21 -06:00
Michael Bucari-Tovo d336848ed0 Change how large cover image viewer loads images 2022-07-15 15:42:34 -06:00
Michael Bucari-Tovo 8cd6219bd9 Performance improvements and better mvvp pattern following 2022-07-15 15:16:27 -06:00
Michael Bucari-Tovo c2a2e51bde Improve re-display function 2022-07-15 13:09:19 -06:00
Michael Bucari-Tovo d62821cd60 Refactor 2022-07-15 01:06:55 -06:00
Michael Bucari-Tovo 180d591b0a Make Form1 MVVM 2022-07-15 00:23:22 -06:00
Michael Bucari-Tovo 7b7e1d8574 Further sorting and remove books refinements 2022-07-14 21:14:40 -06:00
Michael Bucari-Tovo efd6156fa8 Fix STAThread error 2022-07-14 18:25:28 -06:00
Michael Bucari-Tovo 428ea5e864 Improve AvaloniaUI startup times 2022-07-14 17:57:46 -06:00
Michael Bucari-Tovo 2b6d1201b6 Add save and restore form size 2022-07-14 15:41:30 -06:00
Michael Bucari-Tovo de3524d688 refine message box. 2022-07-14 13:26:36 -06:00
Michael Bucari-Tovo 61a529e62b MessageBox revision and more async loading 2022-07-14 12:51:50 -06:00
Michael Bucari-Tovo a5d225dc44 Minor refactor 2022-07-14 02:46:45 -06:00
Michael Bucari-Tovo 7b28a274a8 Startup speedup 2022-07-14 02:35:38 -06:00
Michael Bucari-Tovo 26508e6a8a Speed up start time 2022-07-14 02:18:26 -06:00
Michael Bucari-Tovo c8d91032c0 Refactor 2022-07-14 01:07:07 -06:00
Michael Bucari-Tovo 7a8e910697 Add Avalonia MessageBox 2022-07-14 00:50:50 -06:00
Michael Bucari-Tovo 31d6fc8197 Refactor 2022-07-13 19:03:52 -06:00
Michael Bucari-Tovo e23e267d17 Add column customizations 2022-07-13 18:47:43 -06:00
Michael Bucari-Tovo c727286d22 Move ProcessQueue biz logic into viewmodel 2022-07-13 17:06:18 -06:00
Michael Bucari-Tovo 3a61c32881 Fix sorting and refactor 2022-07-13 16:07:05 -06:00
Michael Bucari-Tovo e33fd6ea1b Default invisible 2022-07-13 02:23:55 -06:00
Michael Bucari-Tovo aa8e3ac09b More sorting hacking 2022-07-13 02:21:05 -06:00
Michael Bucari-Tovo eb49dcfc54 Incremental prgress. 2022-07-13 01:14:05 -06:00
Michael Bucari-Tovo 6182b2bcee Improve styles and fix sotring of podcasts when they are collapsed. 2022-07-12 22:01:11 -06:00
Michael Bucari-Tovo 6e091230cf Use ReactiveUI.
Sort of fix remove book checkbox column.
2022-07-12 18:56:25 -06:00
Michael Bucari-Tovo 5f45d28b9f Refinements 2022-07-12 00:18:56 -06:00
Michael Bucari-Tovo f8e9c16bc1 Change some defaults 2022-07-11 21:57:41 -06:00
Michael Bucari-Tovo a66b7a6eab Add queue log and improve display styles 2022-07-11 21:43:20 -06:00
Michael Bucari-Tovo 3b42b52ff4 Improve sorting 2022-07-11 19:07:20 -06:00
Michael Bucari-Tovo df5293ce1e Fix bug caused by moving column before frozen "Remove" column 2022-07-11 12:58:20 -06:00
Michael Bucari-Tovo 664ff6aabd Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-11 00:13:41 -06:00
Michael Bucari-Tovo 0de62ce010 Port Form1 to Avalonia 2022-07-11 00:13:32 -06:00
Robert McRackan 9eafbacad9 Add Audible ID to BookDetailsDialog 2022-07-08 23:04:05 -04:00
Robert McRackan 058eb31110 trivial change to test local github account settings 2022-07-08 21:43:10 -04:00
Robert McRackan 29de8f5706 trivial change to test github settings 2022-07-08 21:42:17 -04:00
Robert McRackan ef869dbe09 new publish settings moved to Libation's settings in GitHubReleaser
old sizes
zipped 70.5 MB
unzipped 164 MB

new sizes
zipped 22.5 MB
unzipped 52.4 MB
2022-07-08 14:59:31 -04:00
Robert McRackan 9f8b320493 no longer needed 2022-07-07 21:43:51 -04:00
Robert McRackan ef72e04be3 Oops! I only meant to get rid of the bump, not the embedded. Reverting 2022-07-07 16:47:08 -04:00
Robert McRackan 38d280b7f4 v8.1.7 2022-07-07 14:22:05 -04:00
Robert McRackan 468356d676 increm ver 2022-07-06 16:52:37 -04:00
rmcrackan 7364700899 Merge pull request #305 from Mbucari/master
Fix some bugs with user settings.
2022-07-06 16:50:32 -04:00
Michael Bucari-Tovo e65f19cf24 Restore tool 2022-07-06 14:23:03 -06:00
Michael Bucari-Tovo 4272dfe03d Reformat for style 2022-07-06 14:18:53 -06:00
Michael Bucari-Tovo 3b739328fb Fix some bugs with user settings. 2022-07-06 13:10:37 -06:00
Robert McRackan 81c3dca740 increm ver 2022-06-26 17:08:55 -04:00
rmcrackan dceb3121b1 Merge pull request #300 from Mbucari/master
Option to combine Opening/End Credits chapters and other changes
2022-06-26 16:12:17 -04:00
Michael Bucari-Tovo cb60a97b91 Embed PDBs 2022-06-26 13:26:36 -06:00
Michael Bucari-Tovo eb658396d2 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-06-26 13:16:31 -06:00
Michael Bucari-Tovo 0a1cefdb76 Update audible version 2022-06-26 13:16:27 -06:00
Robert McRackan fb618e6719 api bug fix 2022-06-26 15:09:07 -04:00
Mbucari 2d529539cd Merge branch 'rmcrackan:master' into master 2022-06-26 13:02:02 -06:00
Michael Bucari-Tovo 9d93a98a58 Update reference 2022-06-26 13:01:40 -06:00
Michael Bucari-Tovo 38dcb10a6e Update reference 2022-06-26 12:59:02 -06:00
Michael Bucari-Tovo 50651339ec Don't throw on unidentified series. 2022-06-26 11:40:48 -06:00
Robert McRackan d0b2889fec Bug fix #294 mp3s which are split by chapter 2022-06-26 12:55:19 -04:00
Michael Bucari-Tovo 3ce1f94f87 Revert preview feature 2022-06-26 10:42:52 -06:00
Michael Bucari-Tovo 888967be31 Pack files, not folder. 2022-06-26 01:22:46 -06:00
Michael Bucari-Tovo 6826237657 Use powershell script to publish and zip libation 2022-06-26 01:16:17 -06:00
Michael Bucari-Tovo a8987cf1d3 Only increment build number on debug builds 2022-06-25 17:06:28 -06:00
Michael Bucari-Tovo d48a74912a Use abstract static member, add publish script 2022-06-25 16:48:23 -06:00
Mbucari 1668b7c9a1 Merge branch 'rmcrackan:master' into master 2022-06-25 13:43:50 -06:00
Robert McRackan efa2cfb50b Bug fix #294 2022-06-25 14:50:14 -04:00
Michael Bucari-Tovo 071b1a54d5 Publish Embedded 2022-06-25 05:11:21 -06:00
Michael Bucari-Tovo 7c3bba2ffd Merge branch 'master' of https://github.com/Mbucari/Libation 2022-06-24 23:27:53 -06:00
Michael Bucari-Tovo d58092968a Add option to merge Opening/End Credits with following/preceding chapters 2022-06-24 23:26:52 -06:00
Michael Bucari-Tovo 1b20bb06ad Add some filename length headroom in case of diplicate files and " (n)" suffix. 2022-06-24 23:23:08 -06:00
Michael Bucari-Tovo 5815a04712 Add bitrate to Book 2022-06-24 23:09:20 -06:00
Robert McRackan 85c449bec0 Logging and error handling for issue #294 2022-06-24 07:27:26 -04:00
rmcrackan 10bdddb262 Merge pull request #296 from Mbucari/master
Error handling inside all click handlers
2022-06-24 07:15:32 -04:00
Michael Bucari-Tovo b65875386d Add error handling to ProductsGrid.DataGridView_CellContentClick 2022-06-23 22:32:43 -06:00
Michael Bucari-Tovo 76b5e09f72 Add error handling around all ui click handlers for book downloads. 2022-06-23 21:17:43 -06:00
Michael Bucari-Tovo 0fe07695b2 Make better use of hierarchical chapters and add test 2022-06-23 20:45:41 -06:00
Michael Bucari-Tovo 51f9b4f473 More character replacement safety 2022-06-23 20:45:09 -06:00
Robert McRackan 153e1b92bf Bug fixes, logging, options for how to handle illegal characters 2022-06-23 21:01:44 -04:00
rmcrackan fc5ae7403a Merge pull request #295 from Mbucari/master
Optional illegal character replacement and more error handling/logging
2022-06-23 20:56:58 -04:00
Michael Bucari-Tovo 13149eff08 Make better use of heirarch chapters to combine section title audio (which is usually short, eg "Part 1") with the following full-length chapter. 2022-06-23 17:29:45 -06:00
Michael Bucari-Tovo 9c53d9bf87 Better open/close quote detection 2022-06-23 16:52:13 -06:00
Michael Bucari-Tovo bc9625fece Disallow illegal chars in templates 2022-06-23 16:36:56 -06:00
Michael Bucari-Tovo 7e00162ef2 Code reuse and better naming 2022-06-23 16:28:21 -06:00
Michael Bucari-Tovo af38750e29 Fix reverted changes 2022-06-23 16:19:00 -06:00
Michael Bucari-Tovo 314f4850bc Add logging and error handling to Process Queue. and Processables 2022-06-23 15:38:39 -06:00
Michael Bucari-Tovo 9ff2a83ba3 Rename Minimum to Barebones 2022-06-23 13:11:35 -06:00
Michael Bucari-Tovo 2ab466c570 Custom illegal character replacement 2022-06-23 13:01:24 -06:00
Mbucari 184ba84600 Merge branch 'rmcrackan:master' into master 2022-06-23 11:35:09 -06:00
Michael Bucari-Tovo 99dddb1af4 Revert "* bug fix: occasional hang bug in process queue"
This reverts commit b7fd87b09c.
2022-06-23 11:34:50 -06:00
Michael Bucari-Tovo 48eca3f5af Revert "Add character replacement"
This reverts commit 1470aefd42.
2022-06-23 11:34:39 -06:00
Michael Bucari-Tovo 71192cc2ee Revert "Match rmcrackan's changes"
This reverts commit 52622fadbb.
2022-06-23 11:34:29 -06:00
Michael Bucari-Tovo 29c7344540 Revert "linux + WINE link"
This reverts commit eff2634b32.
2022-06-23 11:34:24 -06:00
Michael Bucari-Tovo 6411d23744 Revert "Non-null disposed BlockingCollection can throw exception"
This reverts commit ba722487d8.
2022-06-23 11:34:20 -06:00
Michael Bucari-Tovo 1a74736115 Revert "Improve display and function of character replacement"
This reverts commit b698697256.
2022-06-23 11:34:11 -06:00
Michael Bucari-Tovo 7c11ecb3a7 Revert "Change type"
This reverts commit 839a62cb07.
2022-06-23 11:34:07 -06:00
Michael Bucari-Tovo fd7c833de0 Revert "make auto-scan more fault-tolerant"
This reverts commit f802d1524f.
2022-06-23 11:34:00 -06:00
Michael Bucari-Tovo 7fec8b0d7e Merge branch 'master' of https://github.com/Mbucari/Libation 2022-06-23 11:07:18 -06:00
Michael Bucari-Tovo 52622fadbb Match rmcrackan's changes 2022-06-23 11:07:10 -06:00
Robert McRackan 57255e0aec comments 2022-06-23 07:53:12 -04:00
rmcrackan 17ecfa132d Merge pull request #293 from Dr-Blank/master
Spellcheck in Comments and Strings
2022-06-23 06:53:54 -04:00
Dr-Blank d1365c3d7d Spellcheck in Comments and Strings
Corrected some spellings in Display messages and Comments.
2022-06-22 23:35:54 -04:00
Robert McRackan c33891a4bc update dependencies 2022-06-22 22:13:56 -04:00
Michael Bucari-Tovo 9a63f57147 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-06-22 08:01:48 -06:00
Michael Bucari-Tovo 839a62cb07 Change type 2022-06-22 08:01:39 -06:00
Mbucari dc598e466e Merge branch 'rmcrackan:master' into master 2022-06-21 23:40:21 -06:00
Michael Bucari-Tovo b698697256 Improve display and function of character replacement 2022-06-21 23:39:24 -06:00
Robert McRackan f802d1524f make auto-scan more fault-tolerant 2022-06-21 22:44:25 -04:00
Mbucari 0cb18f9e1a Merge branch 'rmcrackan:master' into master 2022-06-21 20:17:27 -06:00
Robert McRackan ba722487d8 Non-null disposed BlockingCollection can throw exception 2022-06-21 21:08:49 -04:00
rmcrackan eff2634b32 linux + WINE link 2022-06-21 20:54:38 -04:00
Michael Bucari-Tovo 1470aefd42 Add character replacement 2022-06-21 18:50:30 -06:00
Robert McRackan b7fd87b09c * bug fix: occasional hang bug in process queue
* bug fix: #283 template folders
2022-06-21 10:42:57 -04:00
rmcrackan ab82a1656d Merge pull request #282 from Mbucari/master
Fixed rare bug that would hang if an error occured while downloading
2022-06-21 10:34:31 -04:00
Michael Bucari-Tovo 71387e94d8 Fix bug if folder ended in trailing slash 2022-06-21 08:08:09 -06:00
Michael Bucari-Tovo 503379079b Fix WaitToPosition logic 2022-06-21 00:23:02 -06:00
Michael Bucari-Tovo 1ae767087f Check downloadEnded inside WaitToPosition 2022-06-20 23:13:34 -06:00
Michael Bucari-Tovo cfd2b7b7aa Fixed rare bug that would cause a hang if an error occured in the download loop 2022-06-20 22:36:14 -06:00
Robert McRackan 2c42b4c585 * #278 -- new hier. chapters format
* #281 -- template bug fix

Thanks for the quick turn-around, @MBucari !
2022-06-20 21:02:15 -04:00
rmcrackan d3a9ff539e Merge pull request #280 from Mbucari/master
Add support for Audible's new  hierarchical chapters
2022-06-20 20:57:06 -04:00
Michael Bucari-Tovo 58f01bd642 Fix possible x-thread error. 2022-06-20 18:45:44 -06:00
Michael Bucari-Tovo 38806740e1 Use Path.Join instead of string.Join 2022-06-20 18:12:29 -06:00
Michael Bucari-Tovo df583e73c2 Fixed file naming template 2022-06-20 17:37:52 -06:00
Michael Bucari-Tovo e787d33e5a Fix NRE on cancel when there's nothing to cancel. 2022-06-20 16:47:25 -06:00
Michael Bucari-Tovo 91db665428 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-06-20 15:45:00 -06:00
Michael Bucari-Tovo 94d155cff2 Add support for Audible's new hierarchical chapters. 2022-06-20 15:41:37 -06:00
Robert McRackan ad79075fd7 Fix issues #183 and #186 2022-06-20 16:30:40 -04:00
rmcrackan 7baefe2f44 Merge pull request #277 from Mbucari/master
Issues #183 and #186, and a lot of other little things
2022-06-20 16:29:20 -04:00
Michael Bucari-Tovo 141a4c29bb Correct error in saving settings 2022-06-20 14:04:03 -06:00
Michael Bucari-Tovo b2992da370 Move DownloadOptions to FileLiberator 2022-06-20 10:22:21 -06:00
Michael Bucari-Tovo fdee254020 Only copy files if conversion succeeded. 2022-06-20 09:04:06 -06:00
Michael Bucari-Tovo c51489ac74 Await cancell 2022-06-19 18:49:47 -06:00
Michael Bucari-Tovo 3cd394ec10 Change unicode asterisk 2022-06-19 18:04:00 -06:00
Michael Bucari-Tovo 8374fea776 Update tests for unicode chars 2022-06-19 17:59:16 -06:00
Michael Bucari-Tovo 733ca891de Fix unicode replacement 2022-06-19 17:29:46 -06:00
Michael Bucari-Tovo 490d121db3 Add unicode replacements for illegal characters 2022-06-19 16:57:44 -06:00
Michael Bucari-Tovo 45c5efffbd Add support for multipart title naming templates 2022-06-19 15:42:21 -06:00
Michael Bucari-Tovo a24c929acf Update tests for long file paths 2022-06-19 15:38:59 -06:00
Michael Bucari-Tovo 86a39f10d1 Formatting 2022-06-19 12:59:35 -06:00
Michael Bucari-Tovo 4658afdc20 Add Track Number support and make Cancel async 2022-06-19 12:56:33 -06:00
Michael Bucari-Tovo ae6c2afb30 Improve filename template 2022-06-18 13:04:57 -06:00
Michael Bucari-Tovo a3844a3535 Add long path support 2022-06-18 11:28:48 -06:00
Michael Bucari-Tovo b710075544 Make use of unauthenticated API 2022-06-17 23:09:22 -06:00
Mbucari c4c9786050 Merge branch 'rmcrackan:master' into master 2022-06-17 16:46:31 -06:00
Robert McRackan b4cc81139a Bug fix ( #276 ): x-thread error on fresh install 2022-06-17 12:40:37 -04:00
Mbucari fb20eb9162 Merge branch 'rmcrackan:master' into master 2022-06-15 14:22:09 -06:00
Robert McRackan 263987d2c9 Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-06-15 10:43:04 -04:00
Robert McRackan 0b30a35383 updated dependencies 2022-06-15 10:42:56 -04:00
Mbucari 47df1fc602 Merge branch 'rmcrackan:master' into master 2022-06-14 10:46:00 -06:00
rmcrackan d8375454b9 Merge pull request #274 from maaximal/spelling
Fix spelling error
2022-06-14 09:16:05 -04:00
Max Byszio ad535501c4 Fix spelling error 2022-06-14 14:43:21 +02:00
Michael Bucari-Tovo 159f5cbd00 Add lame options to ConvertToMp3 2022-06-13 22:18:00 -06:00
Michael Bucari-Tovo 2bc74d5378 Combine Streamable and Processable, remove unused events. 2022-06-13 21:40:37 -06:00
Robert McRackan eb513f563e Allow sorting by "Remove" column 2022-06-13 13:55:39 -04:00
rmcrackan 09dc5e9846 Merge pull request #273 from Mbucari/master
Add option to save episodes to series parent
2022-06-13 12:00:05 -04:00
Mbucari cf35a87d85 Update AppScaffolding.csproj 2022-06-12 19:48:51 -06:00
Michael Bucari-Tovo 9f25f619a8 Formatting 2022-06-12 19:37:42 -06:00
Michael Bucari-Tovo 7e989c730c Add option to save podcasts to series folder 2022-06-12 19:36:18 -06:00
Robert McRackan 0926e86956 Version 8 2022-06-12 21:20:02 -04:00
rmcrackan 75967730fd Merge pull request #271 from Mbucari/master
Move Remove Books function into main grid, added more db migrations and fixups for episodes
2022-06-12 21:14:51 -04:00
Michael Bucari-Tovo a3be3e354f Code readability changes 2022-06-12 17:30:11 -06:00
Michael Bucari-Tovo 58c52196f1 Remove Books button now on Main button row 2022-06-12 17:03:29 -06:00
Michael Bucari-Tovo b7b49a60cf Migration Exception handling 2022-06-12 16:35:48 -06:00
Michael Bucari-Tovo fa195483d6 Set Import/Esport initial directory 2022-06-12 16:29:33 -06:00
Michael Bucari-Tovo 2341f6ea3b Better display and hiding of process queue 2022-06-12 16:29:06 -06:00
Michael Bucari-Tovo ffe0f0730d Don't fire click for error books 2022-06-12 15:27:10 -06:00
Michael Bucari-Tovo 23b512910e Update 2022-06-12 15:23:55 -06:00
Michael Bucari-Tovo b1c624b104 Revised stoplight icons 2022-06-12 15:17:07 -06:00
Michael Bucari-Tovo fe35be6682 New libation icons 2022-06-12 13:39:35 -06:00
Michael Bucari-Tovo 2d3eb29bd5 Move event invoke out of lock 2022-06-11 19:10:08 -06:00
Michael Bucari-Tovo 26f0ff62df Additional safety check 2022-06-11 15:10:18 -06:00
Michael Bucari-Tovo 5e145846bd Only check non-liberated books when doing scan remove books. 2022-06-11 12:42:00 -06:00
Michael Bucari-Tovo 1ae5f99bf0 Add migration to try and fix db for incorrect or missing espiode series entries. 2022-06-11 12:41:20 -06:00
Michael Bucari-Tovo 984119c7ee Exit download loop if zero bytes are read. 2022-06-10 21:00:04 -06:00
Michael Bucari-Tovo f8f5eac109 Refactor 2022-06-10 20:45:10 -06:00
Michael Bucari-Tovo 4111d5fa48 Remove redundant declarations. 2022-06-10 19:37:50 -06:00
Michael Bucari-Tovo 2eca9056b9 Reorder api calls 2022-06-10 19:36:00 -06:00
Michael Bucari-Tovo 60e96572ff Always refresh token, regardless of expiration date. 2022-06-10 19:34:49 -06:00
Michael Bucari-Tovo 52193933b2 Add scan and remove books tomain view, remove separate dialog. 2022-06-10 19:22:54 -06:00
Michael Bucari-Tovo 7bcabdda38 FindInactiveBooks now fires ScanBegin and ScanEnd events 2022-06-10 18:30:16 -06:00
Mbucari d993941c4d Merge branch 'rmcrackan:master' into master 2022-06-10 15:41:07 -06:00
Michael Bucari-Tovo b447bff9a6 Add audible-cli import/export accounts 2022-06-10 15:19:05 -06:00
Robert McRackan 73cb5ffba4 clearly Hoopla integration isn't going to happen. delete temp files 2022-06-09 17:01:33 -04:00
Robert McRackan 7d694229c1 Prep for version release 2022-06-08 14:48:44 -04:00
rmcrackan cdb6c9a1a4 Merge pull request #268 from Mbucari/master
Address issues in 263
2022-06-08 14:46:17 -04:00
Michael Bucari-Tovo cc1d2b423f Fix an oopsie 2022-06-08 12:15:21 -06:00
Michael Bucari-Tovo 508e031143 Move all event invocations outside locks 2022-06-08 12:08:15 -06:00
Michael Bucari-Tovo 5a093a9a04 add event keyword 2022-06-08 10:53:45 -06:00
Michael Bucari-Tovo 074d647d19 Improve Query 2022-06-08 10:36:06 -06:00
Michael Bucari-Tovo 6cb98f99c5 Use new content type queries 2022-06-08 10:34:05 -06:00
Michael Bucari-Tovo 7d28681b23 Move queries into DataLayer 2022-06-08 10:08:18 -06:00
Michael Bucari-Tovo 859a8e933c Formatting 2022-06-08 09:46:11 -06:00
Michael Bucari-Tovo a476d5986d Update dependency 2022-06-08 09:44:06 -06:00
Michael Bucari-Tovo 31812bc2d9 Refactoring 2022-06-08 09:24:06 -06:00
Michael Bucari-Tovo 30ba69eca7 Minor refactoring. 2022-06-08 08:52:25 -06:00
Michael Bucari-Tovo cf1bc1c252 By defauly, only get actual books and not parents from DB 2022-06-08 08:40:25 -06:00
Michael Bucari-Tovo ee109ba67d Refactor 2022-06-08 08:39:59 -06:00
Michael Bucari-Tovo 9c6211e8e0 Improve UI speed when adding many books to queue at once. 2022-06-08 08:39:17 -06:00
Michael Bucari-Tovo 0729e4ab09 Minor refactor 2022-06-07 15:41:33 -06:00
Michael Bucari-Tovo 5cbe728631 Don't add series parents to list 2022-06-07 15:32:49 -06:00
Michael Bucari-Tovo 920f4df213 Use new ContentType.Parent to add series info to grid display 2022-06-07 15:28:16 -06:00
Michael Bucari-Tovo c48eacd9af Add ContentType.Parent
Import Series parent when only individual episodes are in library
2022-06-07 15:27:18 -06:00
Michael Bucari-Tovo 30e6deeeaa Add migration to cleans DB of 7.10.1 hack 2022-06-07 15:25:52 -06:00
Robert McRackan 5bc76a3160 New debugging tool: "Hangover". Will be packaged with all releases 2022-06-01 11:49:30 -04:00
Robert McRackan 114925ebce Global exception handling. Threadsafe MessageBoxAlertAdminDialog 2022-05-27 13:38:28 -04:00
Robert McRackan 5a80a0cc06 Second bug fix for issue 263 2022-05-27 07:15:15 -04:00
rmcrackan aebefac7e6 Merge pull request #266 from Mbucari/master
Fix my own screwup
2022-05-27 07:10:02 -04:00
Michael Bucari-Tovo b2d0ee41f2 Fix my own screwup 2022-05-26 21:26:56 -06:00
Robert McRackan 9c20250b0a increm ver 2022-05-26 21:10:12 -04:00
rmcrackan b196836fca Merge pull request #264 from Mbucari/master
Fix for episodes with no series link
2022-05-26 20:41:55 -04:00
Michael Bucari-Tovo d9fbcc615a Change flow 2022-05-26 18:06:44 -06:00
Michael Bucari-Tovo fb247fb33f Add better handling for parents and series with no children. 2022-05-26 17:29:55 -06:00
Michael Bucari-Tovo 61f4dbd896 No need to make a new list. 2022-05-26 16:50:43 -06:00
Michael Bucari-Tovo 2c86571818 Better identification of Chilv vs Parent from SeriesBook.Order 2022-05-26 16:49:03 -06:00
Michael Bucari-Tovo 1b2ec67726 Add series info for parent will null order. 2022-05-26 16:43:56 -06:00
Michael Bucari-Tovo 845af854bd Add exception handling to products display 2022-05-26 16:29:40 -06:00
Mbucari 15b6a66d98 Merge branch 'rmcrackan:master' into master 2022-05-26 16:13:13 -06:00
Michael Bucari-Tovo c95ba0764b Fix bug and add groundwork for future feature 2022-05-26 16:11:52 -06:00
Robert McRackan 42c0648ba7 Bug fix #262 : 'file not found' after moved dir 2022-05-26 16:11:03 -04:00
Robert McRackan 0a6e55dcb7 * Much faster library scans
* Libraries of unlimited size now supported (prev limit was 10k)
2022-05-26 11:33:05 -04:00
rmcrackan 99b77decff Merge pull request #260 from Mbucari/master
Throttle episode scanning to 10 concurrent scans.
2022-05-26 11:29:18 -04:00
Robert McRackan 9e2ca4e586 update dependencies 2022-05-26 10:45:57 -04:00
Michael Bucari-Tovo 2e8acfdeef Limnit episode concurrency to 5 2022-05-26 08:44:39 -06:00
Michael Bucari-Tovo 630096e06d Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-26 08:43:33 -06:00
Michael Bucari-Tovo d92d892dc7 logging 2022-05-25 20:45:18 -06:00
Michael Bucari-Tovo a8f41841bd Throttle episode scanning to 10 concurrent scans. 2022-05-25 20:43:12 -06:00
rmcrackan 76954b5a0a Merge pull request #258 from Mbucari/master
Add support for unlimited library size.
2022-05-25 22:00:22 -04:00
Michael Bucari-Tovo c57b184a09 Remove test params 2022-05-25 16:51:25 -06:00
Michael Bucari-Tovo 20ca4e0739 Refactor for clarity. 2022-05-25 16:49:22 -06:00
Mbucari a972ed5e2e Merge branch 'rmcrackan:master' into master 2022-05-25 16:05:31 -06:00
Michael Bucari-Tovo 2b15bc6ebb Count Items as they come in and log total. 2022-05-25 15:11:38 -06:00
Robert McRackan f7a482659c New feature #241 : Auto download episodes after scanning. Setting is on Import Library tab 2022-05-25 15:21:28 -04:00
Robert McRackan 99527453a7 add TODO 2022-05-25 12:56:34 -04:00
Robert McRackan 3408b4637c search engine cleanup 2022-05-25 12:49:24 -04:00
Robert McRackan 3f2899e97e * New event SearchEngineCommands.SearchEngineUpdated
* Clean up redundant event notifications
2022-05-25 10:09:27 -04:00
Michael Bucari-Tovo 562496cfaa Add more logging 2022-05-24 21:36:56 -06:00
Michael Bucari-Tovo 8283f19d6b Parallelize getChildEpisodesAsync 2022-05-24 21:17:59 -06:00
Michael Bucari-Tovo 242909b542 Don't import empty episode 2022-05-24 18:39:47 -06:00
Michael Bucari-Tovo a7b83ad5e0 Remove 10,000 book limitation and simplify episode import 2022-05-24 18:27:20 -06:00
Michael Bucari-Tovo ed66019d9a Cleanup 2022-05-24 18:24:53 -06:00
Michael Bucari-Tovo bc0009be6c Use event return value instead of passing a set delegate. 2022-05-24 15:47:30 -06:00
Michael Bucari-Tovo c88f47eed4 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-24 15:34:09 -06:00
Michael Bucari-Tovo 59de048ced Error handling network error. 2022-05-24 15:33:52 -06:00
Robert McRackan 7987dfb819 Rename 'liberate visible' menu items. Similar names are error-prone 2022-05-24 15:45:56 -04:00
Robert McRackan 1b101106e7 Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-05-24 15:32:42 -04:00
rmcrackan 7b75955aec Merge pull request #257 from Mbucari/master
Fix hang on grid update
2022-05-24 15:32:29 -04:00
Michael Bucari-Tovo 8f5467e6ca Revert stupid change. 2022-05-24 13:30:39 -06:00
Michael Bucari-Tovo 28764f92b9 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-24 13:16:56 -06:00
Michael Bucari-Tovo 777dfe4c62 Fix hang on grid update 2022-05-24 13:16:44 -06:00
Robert McRackan 0878a704d9 search engine: podcast and episode should allow plural 2022-05-24 15:07:53 -04:00
954 changed files with 99915 additions and 20972 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
{
"CdmUrls": [
"https://ollj0gz40d.execute-api.us-west-2.amazonaws.com/default/AudibleCdm"
]
}
+5
View File
@@ -0,0 +1,5 @@
# https://editorconfig.org
root = true
[*.md]
charset = utf-8
+45
View File
@@ -0,0 +1,45 @@
---
name: Bug report
about: Create a report to help us improve Libation
title: ''
labels: bug
assignees: ''
---
PLEASE FILL OUT THE FOLLOWING. Bug reports with limited information or lacking an attached log file may get limited or delayed help.
___
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior
A clear and concise description of what you expected to happen.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Platform
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
## Log Files
Attach your Libation log file here. If your user folder contains the file "LibationCrash.log", attach that also.
**Default Log File Locations**
|Platform|Folder|
|-|-|
|Windows|`%userprofile%\Libation`|
|macOS|`~/Library/Application Support/Libation`|
|Linux|`~/.local/share/Libation`|
**macOS:** If that folder does not exist because Libation never starts, try launching from Terminal and say whether it works: `/Applications/Libation.app/Contents/MacOS/Libation` (see [Troubleshooting for macOS](https://github.com/rmcrackan/Libation/blob/main/docs/advanced/troubleshoot.md#macos)).
Alternative, you may open the log file folder from within Libation. Open Libation's settings, and on the first tab in Settings you can click the button 'Open log folder'.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+31
View File
@@ -0,0 +1,31 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**No-go ideas**
There are lots of great ideas and many are beyond what we intend to do for Libation. Some good ideas which we do not intend to pursue:
* comprehensive api/cli
* aax/audiobook import
* bulk rename of existing files
* general metadata/tag editor
* playback features
* web gui
* supporting non-audible vendors
* official docker support
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+8
View File
@@ -0,0 +1,8 @@
---
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>

After

Width:  |  Height:  |  Size: 303 B

+10
View File
@@ -0,0 +1,10 @@
# Labels required by exempt-creators.yml and stale.yml. Sync with sync-labels workflow.
# (Other repo labels are left unchanged when prune: false.)
---
- name: stale
color: 'eeeeee'
description: 'No activity for an extended period'
- name: exempt
color: '0e8a16'
description: 'Excluded from automatic stale closure (e.g. maintainer-opened)'
+83
View File
@@ -0,0 +1,83 @@
# build-linux.yml
# Reusable workflow that builds the Linux and MacOS (x64 and arm64) versions of Libation.
---
name: build
on:
workflow_call:
inputs:
libation-version:
type: string
required: true
dotnet-version:
type: string
required: true
run-unit-tests:
type: boolean
publish-r2r:
type: boolean
retention-days:
type: number
architecture:
type: string
description: "CPU architecture targeted by the build."
required: true
OS:
type: string
description: >
The operating system targeted by the build.
There must be a corresponding Bundle_$OS.sh script file in ./Scripts
required: true
jobs:
build:
name: "${{ inputs.OS }}-${{ inputs.architecture }}"
runs-on: ubuntu-latest
env:
RUNTIME_ID: "linux-${{ inputs.architecture }}"
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ inputs.dotnet-version }}
dotnet-quality: "ga"
- name: Unit test
if: ${{ inputs.run-unit-tests }}
working-directory: ./Source
run: dotnet test
- name: Publish
id: publish
working-directory: ./Source
run: |
PUBLISH_ARGS=(
'--runtime' '${{ env.RUNTIME_ID }}'
'--configuration' 'Release'
'--output' '../bin'
'-p:PublishProtocol=FileSystem'
"-p:PublishReadyToRun=${{ inputs.publish-r2r }}"
'-p:SelfContained=true')
dotnet publish LibationAvalonia/LibationAvalonia.csproj "${PUBLISH_ARGS[@]}"
dotnet publish LoadByOS/LinuxConfigApp/LinuxConfigApp.csproj "${PUBLISH_ARGS[@]}"
dotnet publish LibationCli/LibationCli.csproj "${PUBLISH_ARGS[@]}"
dotnet publish HangoverAvalonia/HangoverAvalonia.csproj "${PUBLISH_ARGS[@]}"
- name: Build bundle
id: bundle
run: |
SCRIPT=./Scripts/Bundle_${{ inputs.OS }}.sh
chmod +rx ${SCRIPT}
${SCRIPT} ./bin "${{ inputs.libation-version }}" "${{ inputs.architecture }}"
artifact=$(ls ./bundle)
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
- uses: actions/upload-artifact@v7
with:
name: ${{ steps.bundle.outputs.artifact }}
path: ./bundle/${{ steps.bundle.outputs.artifact }}
if-no-files-found: error
retention-days: ${{ inputs.retention-days }}
+113
View File
@@ -0,0 +1,113 @@
# build-mac.yml
# Reusable workflow that builds the MacOS (x64 and arm64) versions of Libation.
---
name: build
on:
workflow_call:
inputs:
libation-version:
type: string
required: true
dotnet-version:
type: string
required: true
run-unit-tests:
type: boolean
publish-r2r:
type: boolean
retention-days:
type: number
sign-app:
type: boolean
description: "Wheather to sign an notorize the app bundle and dmg."
architecture:
type: string
description: "CPU architecture targeted by the build."
required: true
jobs:
build:
name: "macOS-${{ inputs.architecture }}"
runs-on: macos-latest
env:
RUNTIME_ID: "osx-${{ inputs.architecture }}"
WAIT_FOR_NOTARIZE: ${{ vars.WAIT_FOR_NOTARIZE == 'true' }}
steps:
- uses: apple-actions/import-codesign-certs@v7
if: ${{ inputs.sign-app }}
with:
p12-file-base64: ${{ secrets.DISTRIBUTION_SIGNING_CERT }}
p12-password: ${{ secrets.DISTRIBUTION_SIGNING_CERT_PW }}
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ inputs.dotnet-version }}
dotnet-quality: "ga"
- name: Unit test
if: ${{ inputs.run-unit-tests }}
working-directory: ./Source
run: dotnet test
- name: Publish
id: publish
working-directory: ./Source
run: |
PUBLISH_ARGS=(
'--runtime' '${{ env.RUNTIME_ID }}'
'--configuration' 'Release'
'--output' '../bin'
'-p:PublishProtocol=FileSystem'
"-p:PublishReadyToRun=${{ inputs.publish-r2r }}"
'-p:SelfContained=true')
dotnet publish LibationAvalonia/LibationAvalonia.csproj "${PUBLISH_ARGS[@]}"
dotnet publish LoadByOS/MacOSConfigApp/MacOSConfigApp.csproj "${PUBLISH_ARGS[@]}"
dotnet publish LibationCli/LibationCli.csproj "${PUBLISH_ARGS[@]}"
dotnet publish HangoverAvalonia/HangoverAvalonia.csproj "${PUBLISH_ARGS[@]}"
- name: Build bundle
id: bundle
run: |
SCRIPT=./Scripts/Bundle_MacOS.sh
chmod +rx ${SCRIPT}
${SCRIPT} ./bin "${{ inputs.libation-version }}" "${{ inputs.architecture }}" "${{ inputs.sign-app }}"
artifact=$(ls ./bundle)
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
- name: Notarize bundle
if: ${{ inputs.sign-app }}
run: |
if [ ${{ vars.WAIT_FOR_NOTARIZE == 'true' }} ]; then
WAIT="--wait"
fi
echo "::debug::Submitting the disk image for notarization"
# Capture stdout+stderr (2>&1). Use || true so that when notarytool fails (e.g. Apple TOS
# agreement required), the script does not exit before we can print RESPONSE—otherwise the
# job would fail with no visible error message in the workflow log.
RESPONSE=$(xcrun notarytool submit ./bundle/${{ steps.bundle.outputs.artifact }} $WAIT --no-progress --apple-id ${{ vars.APPLE_DEV_EMAIL }} --password ${{ secrets.APPLE_DEV_PASSWORD }} --team-id ${{ secrets.APPLE_TEAM_ID }} 2>&1) || true
echo "$RESPONSE"
SUBMISSION_ID=$(echo "$RESPONSE" | awk '/id: / { print $2;exit; }')
echo "::notice::Notary Submission Id: $SUBMISSION_ID"
# Re-fail the step if submit failed (e.g. no submission id). The job still fails, but the
# output above is now visible in the log so we can see the real error (e.g. sign agreement).
if [ -z "$SUBMISSION_ID" ]; then
echo "::error::Notarization submit failed. See output above for details (e.g. Apple TOS agreement)."
exit 1
fi
if [ ${{ vars.WAIT_FOR_NOTARIZE == 'true' }} ]; then
echo "::debug::Stapling the notarization ticket to the disk image"
xcrun stapler staple "./bundle/${{ steps.bundle.outputs.artifact }}"
fi
- uses: actions/upload-artifact@v7
with:
name: ${{ steps.bundle.outputs.artifact }}
path: ./bundle/${{ steps.bundle.outputs.artifact }}
if-no-files-found: error
retention-days: ${{ inputs.retention-days }}
+130
View File
@@ -0,0 +1,130 @@
# build-windows.yml
# Reusable workflow that builds the Windows versions of Libation.
---
name: build
on:
workflow_call:
inputs:
libation-version:
type: string
required: true
dotnet-version:
type: string
required: true
run-unit-tests:
type: boolean
publish-r2r:
type: boolean
build-installer:
type: boolean
description: "Build *-setup.exe on all Windows matrix jobs when release (zips always built for .releaseindex.json)"
default: false
retention-days:
type: number
jobs:
build:
name: "Windows-${{ matrix.release_name }}-${{ matrix.architecture }} (${{ matrix.ui }})"
runs-on: windows-latest
strategy:
matrix:
ui: [Avalonia]
architecture: [x64]
release_name: [chardonnay]
artifact_stem: [Libation]
include:
- architecture: x64
ui: WinForms
release_name: classic
artifact_stem: Libation-Classic
- architecture: arm64
ui: Avalonia
release_name: chardonnay
artifact_stem: Libation
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ inputs.dotnet-version }}
dotnet-quality: "ga"
- name: Unit test
if: ${{ inputs.run-unit-tests }}
working-directory: ./Source
run: dotnet test
- name: Publish
working-directory: ./Source
run: |
$PUBLISH_ARGS=@(
"--runtime", "win-${{ matrix.architecture }}",
"--configuration", "Release",
"--output", "../bin",
"-p:PublishProtocol=FileSystem",
"-p:PublishReadyToRun=${{ inputs.publish-r2r }}",
"-p:SelfContained=true")
dotnet publish "Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj" $PUBLISH_ARGS
dotnet publish "LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj" $PUBLISH_ARGS
dotnet publish "LibationCli/LibationCli.csproj" $PUBLISH_ARGS
dotnet publish "Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj" $PUBLISH_ARGS
- name: Zip artifact
id: zip
working-directory: ./bin
run: |
$delfiles = @(
"WindowsConfigApp.exe",
"WindowsConfigApp.runtimeconfig.json",
"WindowsConfigApp.deps.json")
foreach ($file in $delfiles){ if (test-path $file){ Remove-Item $file } }
$artifact="${{ matrix.artifact_stem }}.${{ inputs.libation-version }}-windows-${{ matrix.release_name }}-${{ matrix.architecture }}.zip"
"artifact=$artifact" >> $env:GITHUB_OUTPUT
Compress-Archive -Path * -DestinationPath "$artifact"
- uses: actions/upload-artifact@v7
with:
name: ${{ steps.zip.outputs.artifact }}
path: ./bin/${{ steps.zip.outputs.artifact }}
if-no-files-found: error
retention-days: ${{ inputs.retention-days }}
- name: Install Inno Setup
if: ${{ inputs.build-installer }}
run: choco install innosetup -y --no-progress
- name: Build Windows installer
id: setup
if: ${{ inputs.build-installer }}
shell: pwsh
run: |
if ("${{ matrix.ui }}" -eq "WinForms") {
$setupName = "Libation-Classic.${{ inputs.libation-version }}-windows-${{ matrix.release_name }}-${{ matrix.architecture }}-setup.exe"
$ui = "WinForms"
} else {
$setupName = "Libation.${{ inputs.libation-version }}-windows-${{ matrix.release_name }}-${{ matrix.architecture }}-setup.exe"
$ui = "Avalonia"
}
./Scripts/Windows/Build-WindowsInstaller.ps1 `
-Version "${{ inputs.libation-version }}" `
-Ui $ui `
-Architecture "${{ matrix.architecture }}" `
-SkipPublish `
-BinDir "${{ github.workspace }}/bin"
if (-not (Test-Path "${{ github.workspace }}/bin/$setupName")) {
throw "Installer not found: bin/$setupName"
}
"setup=$setupName" >> $env:GITHUB_OUTPUT
- name: Upload installer artifact
if: ${{ inputs.build-installer }}
uses: actions/upload-artifact@v7
with:
name: ${{ steps.setup.outputs.setup }}
path: ./bin/${{ steps.setup.outputs.setup }}
if-no-files-found: error
retention-days: ${{ inputs.retention-days }}
+72
View File
@@ -0,0 +1,72 @@
# build.yml
# Reusable workflow that builds Libation for all platforms.
---
name: build
on:
workflow_call:
inputs:
libation-version:
type: string
description: "Libation version number"
required: true
dotnet-version:
type: string
default: "10.x"
description: ".NET version to target"
run-unit-tests:
type: boolean
description: "Whether to run unit tests prior to publishing."
publish-r2r:
type: boolean
description: "Whether to publish assemblies as ReadyToRun."
release:
type: boolean
description: "Whether this workflow is being called as a release"
retention-days:
type: number
description: "Number of days the artifacts are to be retained."
jobs:
windows:
uses: ./.github/workflows/build-windows.yml
with:
libation-version: ${{ inputs.libation-version }}
dotnet-version: ${{ inputs.dotnet-version }}
run-unit-tests: ${{ inputs.run-unit-tests }}
publish-r2r: ${{ inputs.publish-r2r }}
build-installer: ${{ inputs.release }}
retention-days: ${{ inputs.retention-days }}
macOS:
strategy:
matrix:
architecture: [x64, arm64]
uses: ./.github/workflows/build-mac.yml
with:
libation-version: ${{ inputs.libation-version }}
dotnet-version: ${{ inputs.dotnet-version }}
run-unit-tests: ${{ inputs.run-unit-tests }}
publish-r2r: ${{ inputs.publish-r2r }}
retention-days: ${{ inputs.retention-days }}
architecture: ${{ matrix.architecture }}
sign-app: ${{ inputs.release || vars.SIGN_MAC_APP_ON_VALIDATE == 'true' }}
secrets: inherit
linux:
strategy:
matrix:
OS: [Redhat, Debian]
architecture: [x64, arm64]
uses: ./.github/workflows/build-linux.yml
with:
libation-version: ${{ inputs.libation-version }}
dotnet-version: ${{ inputs.dotnet-version }}
run-unit-tests: ${{ inputs.run-unit-tests }}
publish-r2r: ${{ inputs.publish-r2r }}
retention-days: ${{ inputs.retention-days }}
architecture: ${{ matrix.architecture }}
OS: ${{ matrix.OS }}
+71
View File
@@ -0,0 +1,71 @@
name: Deploy VitePress site to Pages
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [master]
paths:
- .github/workflows/deploy.yml
- docs/**
- .vitepress/**
- index.md
- package.json
- package-lock.json
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v4 # Uncomment this block if you're using pnpm
# with:
# version: 9 # Not needed if you've set "packageManager" in package.json
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v6
- name: Install dependencies
run: npm ci # or pnpm install / yarn install / bun install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
path: .vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
+65
View File
@@ -0,0 +1,65 @@
# docker.yml
# Reusable workflow that builds a docker image for Libation.
---
name: docker
on:
workflow_call:
inputs:
version:
type: string
description: "Version number"
required: true
release:
type: boolean
description: "Is this a release build?"
required: true
secrets:
docker_username:
required: true
docker_token:
required: true
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
if: ${{ inputs.release }}
uses: docker/login-action@v4
with:
username: ${{ secrets.docker_username }}
password: ${{ secrets.docker_token }}
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v6
with:
flavor: |
latest=true
images: |
name=${{ secrets.docker_username }}/libation
tags: |
type=raw,value=${{ inputs.version }},enable=${{ inputs.release }}
- name: Build and push image
uses: docker/build-push-action@v7
with:
# Avoid default git context refs/pull/*/merge (breaks after PR merge or on some re-runs).
context: .
platforms: linux/amd64,linux/arm64
push: ${{ steps.metadata.outputs.tags != ''}}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
@@ -0,0 +1,67 @@
# One-time or occasional backfill: add `exempt` to open issues/PRs opened by
# rmcrackan or Mbucari that predate exempt-creators.yml. Run from Actions →
# "Exempt creators backfill" → Run workflow.
---
name: Exempt creators backfill
on:
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
backfill:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v9
with:
script: |
const exemptLabel = 'exempt';
const creators = new Set(['rmcrackan', 'mbucari']);
const owner = context.repo.owner;
const repo = context.repo.repo;
const items = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
per_page: 100,
});
let labeled = 0;
let notCreator = 0;
let alreadyExempt = 0;
for (const item of items) {
const login = item.user && item.user.login;
if (!login || !creators.has(login.toLowerCase())) {
notCreator++;
continue;
}
const names = item.labels.map((l) => l.name);
if (names.some((n) => n.toLowerCase() === exemptLabel)) {
alreadyExempt++;
continue;
}
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: item.number,
labels: [exemptLabel],
});
labeled++;
} catch (e) {
core.warning(`#${item.number}: ${e.message}`);
}
}
core.info(
`Backfill done: added exempt to ${labeled} item(s); ` +
`${alreadyExempt} already had exempt; ${notCreator} not from rmcrackan/Mbucari.`
);
+39
View File
@@ -0,0 +1,39 @@
# Assigns the `exempt` label to issues and PRs opened by maintainer accounts so
# stale.yml does not mark them stale. See issue #1532.
# For issues/PRs opened before this existed, run workflow "Exempt creators backfill".
# Pattern: https://github.com/Asperguide/asper-header/blob/main/.github/workflows/exempt-creators.yml
---
name: Exempt specific creators
on:
issues:
types: [opened]
pull_request:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
exempt:
runs-on: ubuntu-latest
steps:
- name: Add exempt label
if: >-
(github.event_name == 'issues' &&
contains(fromJSON('["rmcrackan","Mbucari"]'), github.event.issue.user.login)) ||
(github.event_name == 'pull_request' &&
contains(fromJSON('["rmcrackan","Mbucari"]'), github.event.pull_request.user.login))
uses: actions/github-script@v9
with:
script: |
const number = context.issue?.number || context.payload.pull_request?.number;
const repo = context.repo;
if (number) {
await github.rest.issues.addLabels({
...repo,
issue_number: number,
labels: ['exempt'],
});
}
+68
View File
@@ -0,0 +1,68 @@
# release.yml
# Builds and creates the release on any tags starting with a `v`
---
name: release
on:
push:
tags:
- "v*"
jobs:
prerelease:
runs-on: ubuntu-slim
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- name: Get tag version
id: get_version
run: |
export TAG="${{ github.ref_name }}"
echo "version=${TAG#v}" >> "${GITHUB_OUTPUT}"
docker:
needs: [prerelease]
uses: ./.github/workflows/docker.yml
with:
version: ${{ needs.prerelease.outputs.version }}
release: true
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
build:
needs: [prerelease]
uses: ./.github/workflows/build.yml
secrets: inherit
with:
libation-version: ${{ needs.prerelease.outputs.version }}
publish-r2r: true
release: true
release:
needs: [prerelease, build]
runs-on: ubuntu-latest
steps:
- name: Download Chardonnay, Linux, and Mac artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
pattern: "Libation.*"
merge-multiple: true
- name: Download Classic Windows artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
pattern: "Libation-Classic.*"
merge-multiple: true
- name: Release
id: release
uses: softprops/action-gh-release@v3
with:
name: Libation ${{ needs.prerelease.outputs.version }}
body: "<Put a body here>"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
prerelease: false
files: |
artifacts/*
+66
View File
@@ -0,0 +1,66 @@
# Marks inactive issues/PRs stale and closes them after a further quiet period.
# Exempt labels include `enhancement`, `exempt` (from exempt-creators.yml), and others below.
# Pattern: https://github.com/Asperguide/asper-header/blob/main/.github/workflows/stale.yml
---
name: Close stale issues and PRs
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
days-before-stale: 30
days-before-close: 14
operations-per-run: 100
stale-issue-message: |
This issue has been automatically marked as stale because it has not had any activity for 30 days.
It will be closed in 14 days if no further activity occurs.
If this issue is still relevant, please leave a comment to keep it open.
Thank you for your contributions to Libation.
close-issue-message: |
This issue has been automatically closed due to inactivity.
If you believe this issue is still relevant, please reopen it or create a new issue with updated information.
Thank you for your understanding.
stale-issue-label: stale
stale-pr-message: |
This pull request has been automatically marked as stale because it has not had any activity for 30 days.
It will be closed in 14 days if no further activity occurs.
If you're still working on this PR, please leave a comment to keep it open.
Thank you for your contribution.
close-pr-message: |
This pull request has been automatically closed due to inactivity.
If you'd like to continue working on this, please reopen the PR or submit a new one.
Thank you for your contribution.
stale-pr-label: stale
exempt-issue-labels: 'priority: critical,priority: high,security,help wanted,good first issue,needs-discussion,enhancement,exempt'
exempt-pr-labels: 'priority: critical,priority: high,security,enhancement,exempt'
exempt-all-milestones: true
exempt-all-assignees: false
exempt-draft-pr: true
ascending: true
remove-stale-when-updated: true
debug-only: false
+29
View File
@@ -0,0 +1,29 @@
# Ensures labels referenced by exempt-creators / stale workflows exist.
# Optional per #1532 — run workflow_dispatch if labels are missing.
# Pattern: https://github.com/Asperguide/asper-header/blob/main/.github/workflows/sync-labels.yml
---
name: Sync labels
on:
push:
branches: [master]
paths:
- '.github/labels.yml'
workflow_dispatch:
permissions:
issues: write
jobs:
sync-labels:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Sync labels
uses: micnncim/action-label-syncer@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
manifest: .github/labels.yml
prune: false
@@ -0,0 +1,22 @@
name: Validate MetaInfo
"on":
pull_request:
branches: ["master"]
paths:
- .github/workflows/validate-appstream-metainfo.yml
- Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
push:
branches: ["master"]
paths:
- .github/workflows/validate-appstream-metainfo.yml
- Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
jobs:
validate-appstream-metainfo:
runs-on: ubuntu-latest
container:
image: ghcr.io/flathub/flatpak-builder-lint:latest
steps:
- uses: actions/checkout@v6
- name: Check the MetaInfo file
run: flatpak-builder-lint appstream Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
@@ -0,0 +1,21 @@
name: Check desktop file
"on":
pull_request:
branches: ["master"]
paths:
- .github/workflows/validate-desktop-file.yml
- Source/LoadByOS/LinuxConfigApp/Libation.desktop
push:
branches: ["master"]
paths:
- .github/workflows/validate-desktop-file.yml
- Source/LoadByOS/LinuxConfigApp/Libation.desktop
jobs:
validate-desktop-file:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: sudo apt --yes install desktop-file-utils
- name: Check the desktop file
run: desktop-file-validate Source/LoadByOS/LinuxConfigApp/Libation.desktop
+47
View File
@@ -0,0 +1,47 @@
# validate.yml
# Validates that Libation will build on a pull request or push to master.
---
name: validate
on:
push:
branches: [master]
paths:
- Source/**
- .github/workflows/**
pull_request:
branches: [master]
paths:
- Source/**
- .github/workflows/**
jobs:
get_version:
runs-on: ubuntu-slim
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- name: Get version
id: get_version
run: |
wget "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }}/Source/AppScaffolding/AppScaffolding.csproj"
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
build:
needs: [get_version]
uses: ./.github/workflows/build.yml
secrets: inherit
with:
libation-version: ${{ needs.get_version.outputs.version }}
retention-days: 14
run-unit-tests: true
docker:
needs: [get_version]
uses: ./.github/workflows/docker.yml
with:
version: ${{ needs.get_version.outputs.version }}
release: false
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
+13 -1
View File
@@ -184,7 +184,7 @@ publish/
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
@@ -370,3 +370,15 @@ FodyWeavers.xsd
/__TODO.txt
/DataLayer/LibationContext.db
*/bin-Avalonia
# macOS Directory Info
.DS_Store
# JetBrains Rider Settings
**/.idea/
# VitePress
node_modules
.vitepress/cache
.vitepress/dist
+12
View File
@@ -0,0 +1,12 @@
{
"WindowsClassic": "(?:Classic-Libation|Libation-Classic)\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-classic-x64\\.zip",
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-x64\\.zip",
"WindowsAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-arm64\\.zip",
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.deb",
"LinuxAvalonia_DEB": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.deb",
"LinuxAvalonia_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.rpm",
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-x64\\.dmg",
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-arm64\\.deb",
"LinuxAvalonia_Arm64_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-arm64\\.rpm",
"MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-arm64\\.dmg"
}
+118
View File
@@ -0,0 +1,118 @@
import { defineConfig } from "vitepress";
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Libation",
description:
"Libation: Liberate your Library - A free application for downloading your Audible audiobooks",
head: [["link", { rel: "icon", href: "/favicon.ico" }]],
cleanUrls: true,
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: {
light: "/libation_logo_light.svg",
dark: "/libation_logo_dark.svg",
},
footer: {
message: "Released under the GPLv3 License",
},
editLink: {
pattern: "https://github.com/rmcrackan/Libation/edit/master/:path",
},
lastUpdated: true,
nav: [
{ text: "Getting Started", link: "/docs/getting-started" },
{ text: "Docs", link: "/docs/index" },
{
text: "Download",
link: "https://github.com/rmcrackan/Libation/releases/latest",
},
{
text: "Issues & Requests",
link: "https://github.com/rmcrackan/Libation/issues",
},
{ text: "Donate", link: "https://www.paypal.com/paypalme/mcrackan" },
],
sidebar: [
{
items: [
{ text: "Overview", link: "/docs/index" },
{ text: "Getting Started", link: "/docs/getting-started" },
{ text: "FAQ", link: "/docs/frequently-asked-questions" },
{
text: "Issues & Requests",
link: "https://github.com/rmcrackan/Libation/issues",
},
{ text: "Donate", link: "https://www.paypal.com/paypalme/mcrackan" },
],
},
{
text: "Installation",
collapsed: false,
items: [
{ text: "Linux", link: "/docs/installation/linux" },
{ text: "Mac", link: "/docs/installation/mac" },
{ text: "Docker", link: "/docs/installation/docker" },
],
},
{
text: "Features",
collapsed: false,
items: [
{
text: "Audio File Formats",
link: "/docs/features/audio-file-formats",
},
{ text: "Naming Templates", link: "/docs/features/naming-templates" },
{
text: "Searching & Filtering",
link: "/docs/features/searching-and-filtering",
},
{
text: "Easy guide to searching",
link: "/docs/features/lucene",
},
],
},
{
text: "Advanced",
collapsed: false,
items: [
{ text: "Advanced Topics", link: "/docs/advanced/advanced" },
{ text: "Command Line Interface", link: "/docs/advanced/command-line-interface" },
{ text: "Troubleshooting", link: "/docs/advanced/troubleshoot" },
{ text: "Spatial Audio & DRM", link: "/docs/advanced/spatial-audio" },
],
},
{
text: "Development",
collapsed: false,
items: [
{
text: "Getting Started",
link: "/docs/development/getting-started",
},
{ text: "Contribute", link: "/docs/development/contribute" },
{ text: "Website & Docs", link: "/docs/development/website" },
{ text: "Linux Setup (Nix)", link: "/docs/development/nix-linux-setup" },
],
},
],
outline: {
level: "deep",
},
socialLinks: [
{ icon: "github", link: "https://github.com/rmcrackan/Libation" },
],
search: {
provider: "local",
},
},
});
@@ -0,0 +1,259 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { getDetectOS } from './download-detection.js'
const PREVIEW_OPTIONS = [
{ value: '', label: 'Current system' },
{ value: 'windows|x64', label: 'Windows (x64)' },
{ value: 'windows|arm64', label: 'Windows (arm64)' },
{ value: 'mac|x64', label: 'Mac (x64)' },
{ value: 'mac|arm64', label: 'Mac (arm64)' },
{ value: 'linux|x64|', label: 'Linux (x64)' },
{ value: 'linux|x64|Ubuntu', label: 'Linux Ubuntu (x64)' },
{ value: 'linux|x64|Fedora', label: 'Linux Fedora (x64)' },
{ value: 'linux|arm64|', label: 'Linux (arm64)' },
{ value: 'linux|arm64|Ubuntu', label: 'Linux Ubuntu (arm64)' },
{ value: 'linux|arm64|Fedora', label: 'Linux Fedora (arm64)' },
]
const INSTALL_MAC_URL = 'https://getlibation.com/docs/installation/mac'
const INSTALL_LINUX_URL = 'https://getlibation.com/docs/installation/linux'
const status = ref('loading') // 'loading' | 'ready' | 'error'
const errorMessage = ref('')
const cachedAssets = ref([])
const rec = ref(null)
const blockHead = ref('')
const previewValue = ref('')
const installGuideUrl = ref(null)
const installGuideLabel = ref('')
const d = ref(null)
function getPreviewOverrides() {
if (!previewValue.value) return null
const parts = previewValue.value.split('|')
const overrides = { os: parts[0], architecture: parts[1] }
if (parts.length > 2 && parts[2]) overrides.linuxFlavor = parts[2]
return overrides
}
function recommendedForLabel(overrides) {
if (!d.value) return 'Recommended download for your system'
const os = (overrides && overrides.os) || d.value.os
const ver = (overrides && overrides.osVersion) || d.value.osVersion
const arch = (overrides && overrides.architecture) || d.value.architecture
const flavor = (overrides && overrides.linuxFlavor !== undefined) ? overrides.linuxFlavor : d.value.linuxFlavor
if (os === 'windows') return 'Recommended download for Windows (' + arch + ')'
if (os === 'mac') return 'Recommended download for Mac ' + (ver ? ver + ' ' : '') + '(' + arch + ')'
if (os === 'linux') return 'Recommended download for ' + (flavor ? flavor + ' ' : '') + 'Linux (' + arch + ')'
return 'Recommended download for your system'
}
function updateRecommendation() {
if (!cachedAssets.value.length || !d.value) return
const overrides = getPreviewOverrides()
rec.value = d.value.recommendDownload(cachedAssets.value, overrides)
blockHead.value = recommendedForLabel(overrides)
const os = (overrides && overrides.os) || d.value.os
if (os === 'mac') {
installGuideUrl.value = INSTALL_MAC_URL
installGuideLabel.value = 'Install on macOS'
} else if (os === 'linux') {
installGuideUrl.value = INSTALL_LINUX_URL
installGuideLabel.value = 'Install on Linux'
} else {
installGuideUrl.value = null
installGuideLabel.value = ''
}
}
const showReasonWarn = computed(() => {
return rec.value?.reason && rec.value.reason !== 'Recommended for your system.'
})
const showAssets = computed(() => {
if (!rec.value || !d.value || status.value !== 'ready') return []
const libationAssets = cachedAssets.value
const overrides = getPreviewOverrides()
const os = (overrides && overrides.os) || d.value.os
const arch = (overrides && overrides.architecture) || d.value.architecture
return libationAssets.filter((a) => {
const p = d.value.parseAssetFilename(a.name)
if (!p) return false
if (os === 'windows' || os === 'mac' || os === 'linux') {
if (p.platform !== os) return false
}
if (arch === 'x64' || arch === 'arm64') {
if (p.arch !== arch) return false
}
return true
})
})
watch(previewValue, () => updateRecommendation())
onMounted(() => {
d.value = getDetectOS()
fetch('https://api.github.com/repos/rmcrackan/Libation/releases/latest')
.then((res) => (res.ok ? res.json() : Promise.reject(new Error('Failed to load release'))))
.then((release) => {
cachedAssets.value = (release.assets || []).map((a) => ({
name: a.name,
url: a.browser_download_url,
}))
status.value = 'ready'
updateRecommendation()
})
.catch((err) => {
status.value = 'error'
errorMessage.value = err.message || 'Could not load latest release.'
})
})
</script>
<template>
<div class="download-widget-wrapper">
<div v-if="status === 'loading'" class="download-widget recommended">
Loading latest release
</div>
<div v-else-if="status === 'error'" class="download-widget recommended">
<p class="warn">{{ errorMessage }}</p>
</div>
<div v-else class="download-widget recommended">
<div class="block-head">
<svg class="icon-download" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<span>{{ blockHead }}{{ rec?.windowsX64Choice ? ':' : rec?.recommended ? ':' : rec ? '.' : '' }}</span>
<select v-model="previewValue" aria-label="Find recommended download for" class="preview-select">
<option v-for="o in PREVIEW_OPTIONS" :key="o.value || 'current'" :value="o.value">
{{ o.label }}
</option>
</select>
</div>
<!-- Windows x64: Chardonnay + Classic -->
<template v-if="rec?.windowsX64Choice">
<ul class="widget-list">
<li><strong>Chardonnay</strong> (modern UI): <a :href="rec.windowsX64Choice.chardonnay.url" download>{{ rec.windowsX64Choice.chardonnay.name }}</a></li>
<li><strong>Classic</strong> (compact, screenreader-friendly): <a :href="rec.windowsX64Choice.classic.url" download>{{ rec.windowsX64Choice.classic.name }}</a></li>
</ul>
<p class="widget-link"><a :href="rec.classicVsChardonnayFaqUrl" target="_blank" rel="noopener">What's the difference between Classic and Chardonnay?</a></p>
<p v-if="showReasonWarn" class="warn">{{ rec.reason }}</p>
</template>
<!-- Single recommended -->
<template v-else-if="rec?.recommended">
<p class="widget-download"><a :href="rec.recommended.url" download>{{ rec.recommended.name }}</a></p>
<p v-if="installGuideUrl" class="widget-link">
<a :href="installGuideUrl" target="_blank" rel="noopener">{{ installGuideLabel }}</a>
</p>
<p v-if="showReasonWarn" class="warn">{{ rec.reason }}</p>
<div v-if="rec.alternatives?.length" class="alternatives">
{{ rec.alternatives.length === 1 ? 'Another option:' : 'Other options:' }}
<ul>
<li v-for="a in rec.alternatives" :key="a.name">
<a :href="a.url" download>{{ a.name }}</a>
</li>
</ul>
</div>
</template>
<!-- No match -->
<template v-else-if="rec">
<p class="warn">{{ rec.reason || 'Could not recommend a download.' }}</p>
<div class="alternatives">
Available:
<ul>
<li v-for="a in showAssets" :key="a.name">
<a :href="a.url" download>{{ a.name }}</a>
</li>
</ul>
</div>
</template>
</div>
</div>
</template>
<style scoped>
.download-widget-wrapper {
margin: 1.5rem auto;
max-width: 42rem;
}
.download-widget.recommended {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
padding: 0.75rem 1rem;
border-radius: 8px;
font-weight: 600;
}
.download-widget.recommended a {
color: var(--vp-c-brand-1);
text-decoration: underline;
}
.download-widget.recommended a:hover {
text-decoration: underline;
opacity: 0.8;
}
.block-head {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
.block-head .icon-download {
flex-shrink: 0;
width: 1.25rem;
height: 1.25rem;
color: var(--vp-c-brand-1);
}
.preview-select {
margin-left: auto;
font-size: 0.85rem;
padding-right: 1.75rem;
padding-left: 0.5rem;
appearance: none;
background-color: var(--vp-c-bg-soft);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.35rem center;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
cursor: pointer;
}
.widget-list {
margin: 0 0 0 1rem;
padding-left: 0.5rem;
}
.widget-list li strong {
font-weight: 700;
}
.widget-download {
margin: 0.25rem 0;
}
.widget-link {
margin: 0.5rem 0 0;
}
.warn {
background: var(--vp-custom-block-warning-bg);
color: var(--vp-custom-block-warning-text);
padding: 0.5rem 0.75rem;
border-radius: 6px;
margin: 0.5rem 0 0;
font-size: 0.9rem;
font-weight: normal;
}
.alternatives {
font-size: 0.9rem;
font-weight: normal;
color: var(--vp-c-text-2);
margin-top: 0.5rem;
}
.alternatives ul {
margin: 0.25rem 0 0 1rem;
padding-left: 0.5rem;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
<script setup>
import { ref, onMounted, defineAsyncComponent } from 'vue'
import DefaultTheme from 'vitepress/theme'
import { useData } from 'vitepress'
const { frontmatter } = useData()
const { Layout: DefaultLayout } = DefaultTheme
const mounted = ref(false)
onMounted(() => { mounted.value = true })
// Load widget only on client so download-detection.js (navigator) never runs during SSR
const DownloadSuggestionWidget = defineAsyncComponent(() =>
import('./DownloadSuggestionWidget.vue')
)
</script>
<template>
<DefaultLayout>
<template #home-hero-after>
<DownloadSuggestionWidget v-if="frontmatter.layout === 'home' && mounted" />
</template>
</DefaultLayout>
</template>
+24
View File
@@ -0,0 +1,24 @@
/* Custom styles for Libation documentation */
/* Naming Templates page: monospace in the doc column + outline only.
pageClass is applied to .Layout in VitePress; limit scope to #VPContent
so the top nav, sidebar, and site footer keep the default font. */
.Layout.naming-templates-mono #VPContent,
.Layout.naming-templates-mono #VPContent * {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
}
/* Hide certain nav items on tablet devices to prevent horizontal scroll */
@media (min-width: 640px) and (max-width: 959px) {
/* Target specific nav items by their position */
/* Hide "Issues & Requests" and "Donate" links on tablet */
.VPNav .VPNavBar .nav .VPNavBarMenu .VPMenu:nth-child(3) {
display: none;
}
/* Alternative: Use a more specific selector if needed */
.VPNavBarMenuLink[href*="issues"] {
display: none;
}
}
+218
View File
@@ -0,0 +1,218 @@
/**
* Client-side OS and system detection for download recommendation.
* Uses navigator.userAgent and (when available) navigator.userAgentData.
* Call getDetectOS() in the browser only (e.g. in Vue onMounted).
*/
export function getDetectOS() {
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''
const uaData = typeof navigator !== 'undefined' && navigator.userAgentData
function getOS() {
if (uaData?.platform) {
const p = uaData.platform.toLowerCase()
if (p === 'windows') return 'windows'
if (p === 'macos') return 'mac'
if (p === 'linux') return 'linux'
if (p === 'android') return 'android'
if (p === 'ios') return 'ios'
}
if (/\bWindows\b/i.test(ua)) return 'windows'
if (/\bMac\b/i.test(ua) || /\bMacintosh\b/i.test(ua)) return 'mac'
if (/\bLinux\b/i.test(ua) && !/\bAndroid\b/i.test(ua)) return 'linux'
if (/\bAndroid\b/i.test(ua)) return 'android'
if (/\b(iPhone|iPad|iPod)\b/i.test(ua)) return 'ios'
return 'other'
}
function getOSVersion() {
const os = getOS()
if (os === 'windows') {
const ntMatch = ua.match(/Windows NT (\d+\.\d+)/)
if (ntMatch) {
const nt = ntMatch[1]
if (nt === '10.0') return '10 or 11 (NT 10.0)'
if (nt === '6.3') return '8.1'
if (nt === '6.2') return '8'
if (nt === '6.1') return '7'
return `NT ${nt}`
}
return null
}
if (os === 'mac') {
const macMatch = ua.match(/Mac OS X (\d+[._]\d+(?:[._]\d+)?)/)
return macMatch ? macMatch[1].replace(/_/g, '.') : null
}
if (os === 'linux') {
const linuxMatch = ua.match(/Linux ([^\s;)]+)/)
return linuxMatch ? linuxMatch[1] : null
}
if (os === 'android') {
const m = ua.match(/Android (\d+(?:\.\d+)*)/)
return m ? m[1] : null
}
if (os === 'ios') {
const m = ua.match(/OS (\d+[._]\d+(?:[._]\d+)?)/)
return m ? m[1].replace(/_/g, '.') : null
}
return null
}
function getArchitecture() {
if (uaData?.architecture) {
const a = uaData.architecture.toLowerCase()
if (a === 'x86' || a === 'amd64') return a === 'amd64' ? 'x64' : 'x86'
if (a === 'arm' || a === 'arm64' || a === 'aarch64') return a === 'aarch64' ? 'arm64' : a
return a
}
if (/\b(aarch64|arm64|ARM64)\b/i.test(ua)) return 'arm64'
if (/\b(arm|aarch32)\b/i.test(ua) && !/arm64/i.test(ua)) return 'arm'
if (/\b(Wow64|Win64|x64|x86_64|amd64)\b/i.test(ua)) return 'x64'
if (/\b(WOW32|Win32|x86)\b/i.test(ua)) return 'x86'
return 'other'
}
function getLinuxFlavor() {
if (getOS() !== 'linux') return null
if (/\bUbuntu\b/i.test(ua)) return 'Ubuntu'
if (/\bFedora\b/i.test(ua)) return 'Fedora'
if (/\bDebian\b/i.test(ua)) return 'Debian'
if (/\bLinux Mint\b/i.test(ua)) return 'Linux Mint'
if (/\bOpenSUSE\b/i.test(ua)) return 'OpenSUSE'
if (/\bArch\b/i.test(ua)) return 'Arch'
if (/\bCentOS\b/i.test(ua)) return 'CentOS'
if (/\bRed Hat\b/i.test(ua)) return 'Red Hat'
if (/\bChrome OS\b/i.test(ua) || /\bCrOS\b/i.test(ua)) return 'Chrome OS'
return null
}
function parseAssetFilename(name) {
if (!name || typeof name !== 'string') return null
const lower = name.toLowerCase()
let platform = null
if (lower.includes('windows')) platform = 'windows'
else if (lower.includes('macos') || (lower.includes('mac') && !lower.includes('macos'))) platform = 'mac'
else if (lower.includes('linux')) platform = 'linux'
if (!platform) return null
let arch = null
if (lower.includes('arm64') || lower.includes('aarch64')) arch = 'arm64'
else if (lower.includes('amd64') || lower.includes('x64')) arch = 'x64'
else if (lower.includes('x86') || lower.includes('i386')) arch = 'x86'
if (!arch) return null
let packageType = null
if (lower.endsWith('.deb')) packageType = 'deb'
else if (lower.endsWith('.rpm')) packageType = 'rpm'
else if (lower.endsWith('.dmg')) packageType = 'dmg'
else if (lower.endsWith('-setup.exe')) packageType = 'installer'
else if (lower.endsWith('.zip') || lower.endsWith('.msi')) packageType = 'archive'
return { platform, arch, packageType: packageType || 'unknown', name }
}
function recommendDownload(assets, overrides) {
const os = (overrides && overrides.os != null) ? overrides.os : getOS()
const arch = (overrides && overrides.architecture != null) ? overrides.architecture : getArchitecture()
const linuxFlavor = (overrides && overrides.linuxFlavor !== undefined) ? overrides.linuxFlavor : getLinuxFlavor()
let reason = ''
if (!Array.isArray(assets) || assets.length === 0) {
return { recommended: null, alternatives: [], reason: reason || 'No assets provided.' }
}
const wantArch = (arch === 'x64' || arch === 'x86') ? 'x64' : (arch === 'arm64' || arch === 'arm') ? 'arm64' : null
const wantOs = os === 'windows' ? 'windows' : os === 'mac' ? 'mac' : os === 'linux' ? 'linux' : null
const preferDeb = linuxFlavor === 'Ubuntu' || linuxFlavor === 'Debian' || linuxFlavor === 'Linux Mint' || linuxFlavor === 'Chrome OS'
const preferRpm = linuxFlavor === 'Fedora' || linuxFlavor === 'Red Hat' || linuxFlavor === 'CentOS' || linuxFlavor === 'OpenSUSE'
const parsed = []
for (let i = 0; i < assets.length; i++) {
const a = assets[i]
const name = a && (a.name != null ? a.name : a)
const url = a && a.url
const p = parseAssetFilename(name)
if (!p) continue
parsed.push({ name, url, parsed: p })
}
if (parsed.length === 0) {
return { recommended: null, alternatives: [], reason: reason || 'No recognizable asset filenames.' }
}
function score(entry) {
const p = entry.parsed
const nameLower = (entry.name || '').toLowerCase()
let s = 0
if (wantOs && p.platform === wantOs) s += 100
else if (wantOs) s -= 50
if (wantArch && p.arch === wantArch) s += 50
else if (wantArch) s -= 30
if (wantOs === 'linux' && p.packageType === 'deb' && preferDeb) s += 20
if (wantOs === 'linux' && p.packageType === 'rpm' && preferRpm) s += 20
if (wantOs === 'linux' && !preferDeb && !preferRpm && p.packageType === 'deb') s += 5
if (wantOs === 'windows' && p.packageType === 'installer') s += 30
if (wantOs === 'windows' && p.packageType === 'archive') s -= 10
if (nameLower.includes('chardonnay')) s += 10
if (nameLower.includes('classic')) s -= 5
return s
}
parsed.sort((a, b) => score(b) - score(a))
const best = parsed[0]
const bestScore = score(best)
const recommended = bestScore >= 50 ? { name: best.name, url: best.url } : null
let altCandidates = parsed.slice(1, 4)
if (wantOs) altCandidates = altCandidates.filter(e => e.parsed.platform === wantOs)
if (wantArch) altCandidates = altCandidates.filter(e => e.parsed.arch === wantArch)
const alternatives = altCandidates.map(e => ({ name: e.name, url: e.url }))
const classicVsChardonnayFaqUrl = 'https://getlibation.com/docs/frequently-asked-questions#what-s-the-difference-between-classic-and-chardonnay'
function isWindowsSetup(name) {
return (name || '').toLowerCase().endsWith('-setup.exe')
}
function preferWindowsAsset(current, candidate) {
if (!candidate) return current
if (!current) return candidate
const candSetup = isWindowsSetup(candidate.name)
const currSetup = isWindowsSetup(current.name)
if (candSetup && !currSetup) return candidate
return current
}
function isClassicWindowsAsset(nameLower) {
return nameLower.includes('libation-classic') || nameLower.includes('classic-libation')
}
let windowsX64Choice = null
if (wantOs === 'windows' && wantArch === 'x64') {
let winChardonnay = null
let winClassic = null
for (let j = 0; j < parsed.length; j++) {
const e = parsed[j]
if (e.parsed.platform !== 'windows' || e.parsed.arch !== 'x64') continue
const n = (e.name || '').toLowerCase()
if (n.includes('chardonnay')) winChardonnay = preferWindowsAsset(winChardonnay, e)
else if (isClassicWindowsAsset(n)) winClassic = preferWindowsAsset(winClassic, e)
}
if (winChardonnay && winClassic) {
windowsX64Choice = {
chardonnay: { name: winChardonnay.name, url: winChardonnay.url },
classic: { name: winClassic.name, url: winClassic.url },
}
}
}
if (!recommended && wantOs) {
reason = 'No asset matched your system (OS: ' + os + ', arch: ' + arch + '). Try choosing a download manually.'
} else if (recommended && wantOs && !wantArch) {
reason = 'Matched your OS; architecture could not be detected. If this download is wrong, pick another (e.g. arm64 vs x64).'
}
return {
recommended,
alternatives,
reason: reason || (recommended ? 'Recommended for your system.' : ''),
windowsX64Choice,
classicVsChardonnayFaqUrl: windowsX64Choice ? classicVsChardonnayFaqUrl : null,
}
}
return {
os: getOS(),
osVersion: getOSVersion(),
architecture: getArchitecture(),
linuxFlavor: getLinuxFlavor(),
getOS,
getOSVersion,
getArchitecture,
getLinuxFlavor,
parseAssetFilename,
recommendDownload,
}
}
+8
View File
@@ -0,0 +1,8 @@
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'
import './custom.css'
export default {
extends: DefaultTheme,
Layout,
}
+32
View File
@@ -0,0 +1,32 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console) Windows",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Source/bin/Avalonia/Debug/Libation.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": ".NET Core Launch (console) Linux",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build_linux",
"program": "${workspaceFolder}/Source/bin/Avalonia/Debug/Libation.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
}
]
}
+59
View File
@@ -0,0 +1,59 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "build",
"dependsOn": [
"build_libation",
"build_linuxconfigapp"
]
},
{
"label": "build_libation",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/Source/LibationAvalonia/LibationAvalonia.csproj"
],
"group": "build",
"presentation": {
//"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "build_linuxconfigapp",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/Source/LoadByOS/LinuxConfigApp/LinuxConfigApp.csproj"
],
"group": "build",
"presentation": {
//"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "build_linux",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/Source/LibationAvalonia/LibationAvalonia.csproj",
"-p:TargetFramework=net9.0",
"-p:TargetFrameworks=net9.0",
"-p:RuntimeIdentifier=linux-x64"
],
"group": "build",
"presentation": {
//"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<EnableMSTestRunner>true</EnableMSTestRunner>
</PropertyGroup>
</Project>
+3
View File
@@ -0,0 +1,3 @@
{
"LibationFiles": "/config-internal"
}
+201
View File
@@ -0,0 +1,201 @@
#!/bin/bash
error() {
log "ERROR" "$1"
}
warn() {
log "WARNING" "$1"
}
info() {
log "info" "$1"
}
debug() {
if [ "${LOG_LEVEL}" = "debug" ]; then
log "debug" "$1"
fi
}
log() {
LEVEL=$1
MESSAGE=$2
printf "$(date '+%F %T') %s: %s\n" "${LEVEL}" "${MESSAGE}"
}
init_config_file() {
FILE=$1
FULLPATH=${LIBATION_CONFIG_DIR}/${FILE}
if [ -f ${FULLPATH} ]; then
info "loading ${FILE}"
cp ${FULLPATH} ${LIBATION_CONFIG_INTERNAL}/
return 0
else
warn "${FULLPATH} not found, creating empty file"
echo "{}" > ${LIBATION_CONFIG_INTERNAL}/${FILE}
return 1
fi
}
update_settings() {
FILE=$1
KEY=$2
VALUE=$3
info "setting ${KEY} to ${VALUE}"
echo $(jq --arg k "${KEY}" --arg v "${VALUE}" '.[$k] = $v' ${LIBATION_CONFIG_INTERNAL}/${FILE}) > ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp
mv ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp ${LIBATION_CONFIG_INTERNAL}/${FILE}
}
is_mounted() {
DIR=$1
if grep -qs "${DIR} " /proc/mounts;
then
return 0
else
return 1
fi
}
create_db() {
DBFILE=$1
if [ -f "${DBFILE}" ]; then
warn "prexisting database found when creating"
return 0
else
if ! touch "${DBFILE}"; then
error "unable to create database, check permissions on host"
exit 1
fi
return 1
fi
}
setup_db() {
local DBPATH=$1
local dbpattern="*.db"
local FILE LINK LINK_ORIGIN
local -a files
debug "using database directory ${DBPATH}"
# Figure out the right databse file
if [[ -z $LIBATION_DB_FILE ]]
then
shopt -s nullglob # files shall be empty if no match is found
files=( "${DBPATH}"/${dbpattern} )
shopt -u nullglob
case ${#files[@]} in
0)
FILE="${DBPATH}/LibationContext.db"
;;
1)
FILE="${files[0]}"
;;
*)
debug "found ${#files[@]} database files matching '${dbpattern}'"
error "too many database files found, set LIBATION_DB_FILE to the filename you wish to use"
exit 1
;;
esac
else
FILE="${DBPATH}/${LIBATION_DB_FILE}"
fi
debug "planning to use database '${FILE}'"
if [[ -f $FILE ]]
then
info "database found at ${FILE}"
elif [[ $LIBATION_CREATE_DB == "true" ]]
then
warn "database not found, creating one at ${FILE}"
create_db "${FILE}"
else
error "database not found and creation is disabled"
exit 1
fi
LINK="${LIBATION_CONFIG_INTERNAL}/LibationContext.db"
if [[ -L $LINK ]]
then
# check if the link is correct
LINK_ORIGIN=$(readlink -fn "$LINK")
if [[ $LINK_ORIGIN == $(readlink -fn "$FILE") ]]
then
warn "symlink '${LINK_ORIGIN}' to '${LINK}' already established"
return 0
fi
warn "removing existing symlink '${LINK_ORIGIN}' to '${LINK}'"
rm -f "$LINK"
elif [[ -e $LINK ]]
then
error "found blocking file at '${LINK}' - can't create symlink"
exit 1
fi
ln -s "${FILE}" "${LINK}"
}
run() {
info "scanning accounts"
/libation/LibationCli scan
info "liberating books"
/libation/LibationCli liberate
}
main() {
info "initializing libation"
init_config_file AccountsSettings.json
init_config_file Settings.json
info "loading settings"
update_settings Settings.json Books "${LIBATION_BOOKS_DIR:-/data}"
update_settings Settings.json InProgress /tmp
info "loading database"
# If user provides a separate database mount, use that
if is_mounted "${LIBATION_DB_DIR}";
then
DB_LOCATION=${LIBATION_DB_DIR}
# Otherwise, use the config directory
else
DB_LOCATION=${LIBATION_CONFIG_DIR}
fi
setup_db ${DB_LOCATION}
# Try to warn if books dir wasn't mounted in
if ! is_mounted "${LIBATION_BOOKS_DIR}";
then
warn "${LIBATION_BOOKS_DIR} does not appear to be mounted, books will not be saved"
fi
# Let the user know what the run type will be
if [[ -z "${SLEEP_TIME}" ]]; then
SLEEP_TIME=-1
fi
if [ "${SLEEP_TIME}" == -1 ]; then
info "running once"
else
info "running every ${SLEEP_TIME}"
fi
# loop
while true
do
run
# Liberate only once if SLEEP_TIME was set to -1
if [ "${SLEEP_TIME}" == -1 ]; then
break
fi
sleep "${SLEEP_TIME}"
done
info "exiting"
}
main
+42
View File
@@ -0,0 +1,42 @@
# Dockerfile
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG TARGETARCH
COPY Source /Source
RUN dotnet publish \
/Source/LibationCli/LibationCli.csproj \
--os linux \
--arch ${TARGETARCH} \
--configuration Release \
--output /Source/bin/Publish/Linux-chardonnay \
-p:PublishProtocol=FileSystem \
-p:PublishReadyToRun=true \
-p:SelfContained=true
FROM mcr.microsoft.com/dotnet/runtime:10.0
ARG USER_UID=1001
ARG USER_GID=1001
# Set the character set that will be used for folder and filenames when liberating
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV SLEEP_TIME=-1
ENV LIBATION_CONFIG_INTERNAL=/config-internal
ENV LIBATION_CONFIG_DIR=/config
ENV LIBATION_DB_DIR=/db
ENV LIBATION_DB_FILE=
ENV LIBATION_CREATE_DB=true
ENV LIBATION_BOOKS_DIR=/data
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y jq && \
mkdir -m777 ${LIBATION_CONFIG_INTERNAL} ${LIBATION_BOOKS_DIR}
COPY --from=build /Source/bin/Publish/Linux-chardonnay /libation
COPY Docker/* /libation
USER ${USER_UID}:${USER_GID}
CMD ["/libation/liberate.sh"]
+2 -78
View File
@@ -1,79 +1,3 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
# This page has been moved to https://getlibation.com/docs/advanced/advanced
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Advanced: Table of Contents
- [Files and folders](#files-and-folders)
- [Linux and Mac (unofficial)](#linux-and-mac)
- [Settings](#settings)
- [Custom File Naming](#custom-file-naming)
- [Command Line Interface](#command-line-interface)
### Files and folders
To make upgrades and reinstalls easier, Libation separates all of its responsibilities to a few different folders. If you don't want to mess with this stuff: ignore it. Read on if you like a little more control over your files.
* In Libation's initial folder are the files that make up the program. Since nothing else is here, just copy new files here to upgrade the program. Delete this folder to delete Libation.
* In a separate folder, Libation keeps track of all of the files it creates like settings and downloaded images. After an upgrade, Libation might think that's its being run for the first time. Just click ADVANCED SETUP and point to this folder. Libation will reload your library and settings.
* The last important folder is the "books location." This is where Libation looks for your downloaded and decrypted books. This is how it knows which books still need to be downloaded. The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
### Linux and Mac
Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592))
### Settings
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
### Custom File Naming
In Settings, on the Download/Decrypt tab, you can specify the format in which you want your files to be named. As you edit these templates, a live example will be shown. Parameters are listed for folders, files, and files split by chapter including an explanation of what each naming option means. For instance: you can use template `<title short> - <ch# 0> of <ch count> - <ch title>` to create the file `A Study in Scarlet - 04 of 10 - A Flight for Life.m4b`.
These templates apply to GUI and CLI.
### Command Line Interface
Libationcli.exe allows limited access to Libation's functionalities as a CLI.
Warnings about relying solely on on the CLI:
* CLI will not perform any upgrades.
* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
```
help
libationcli --help
verb-specific help
libationcli scan --help
scan all libraries
libationcli scan
scan only libraries for specific accounts
libationcli scan nickname1 nickname2
convert all m4b files to mp3
libationcli convert
liberate all books and pdfs
libationcli liberate
liberate pdfs only
libationcli liberate --pdf
libationcli liberate -p
export library to file
libationcli export --path "C:\foo\bar\my.json" --json
libationcli export -p "C:\foo\bar\my.json" -j
libationcli export -p "C:\foo\bar\my.csv" --csv
libationcli export -p "C:\foo\bar\my.csv" -c
libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
libationcli export -p "C:\foo\bar\my.xlsx" -x
```
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/advanced/advanced.md))
+3
View File
@@ -0,0 +1,3 @@
# This page has been moved to https://getlibation.com/docs/features/audio-file-formats
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/features/audio-file-formats.md))
+3
View File
@@ -0,0 +1,3 @@
# This page has been moved to https://getlibation.com/docs/installation/docker
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/installation/docker.md))
@@ -0,0 +1,3 @@
# This page has been moved to https://getlibation.com/docs/frequently-asked-questions
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/frequently-asked-questions.md))
+2 -137
View File
@@ -1,138 +1,3 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
# This page has been moved to https://getlibation.com/docs/getting-started
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Getting started: Table of Contents
- [Download Libation](#download-libation-1)
- [Installation](#installation)
- [Create Accounts](#create-accounts)
- [Import your library](#import-your-library)
- [Download your books -- DRM-free!](#download-your-books----drm-free)
- [Download PDF attachments](#download-pdf-attachments)
- [Details of downloaded files](#details-of-downloaded-files)
- [Export your library](#export-your-library)
### [Download Libation](https://github.com/rmcrackan/Libation/releases)
### Installation
To install Libation, extract the zip file to a folder, for example `C:\Libation`, and then run Libation.exe from that folder to begin the configuration process and configure your account(s).
### Create Accounts
Create your account(s):
![Create your accounts, menu](images/v40_accounts.png)
New locale options include many more regions including old audible accounts which pre-date the amazon acquisition
![Choose your account locales](images/v40_locales.png)
### Import your library
Be default, Libation will periodically scan the accounts you added above with a checkbox next to them. Nothing for you to do. You can also scan manually.
Select Import > Scan Library:
![Import step 1](images/Import1.png)
Or if you have multiple accounts, you'll get to choose whether to scan all accounts or just the ones you select:
![Import which accounts](images/v40_import.png)
If this is a new installation, or you're scanning an account you haven't scanned before, you'll be prompted to enter your password for the Audible account.
![Login password](images/alt-login1.png)
Enter the password and click Submit. Audible will prompt you with a CAPTCHA image.
![Login captcha](images/alt-login2.png)
Enter the CAPTCHA answer characters and click Submit. If all has gone well, Libation will start scanning the account.
In rare instances, the Captcha image/response will fail in an endless loop. If this happens, delete the problem account, and then click Save. Re-add the account and click Save again. Now try to scan the account again. This time, instead of typing your password, click the link that says "Or click here". This will open the Audible External Login dialog shown below.
![Login alternative setup](images/alt-login3.png)
You can either copy the URL shown and paste it into your browser or launch the browser directly by clicking Launch in Browser. Audible will display its standard login page. Login, including answering the CAPTCHA on the next page. In some cases, you might have to approve the login from the email account associated with that login, but once the login is successful, you'll see an error message.
![Login alternative login result](images/alt-login4.png)
This actually means you've successfully logged in. Copy the entire URL shown in your browser and return to Libation. Paste that URL into the text box at the bottom of the Audible External Login window and click Submit.
You'll see this window while it's scanning:
![Import step 2](images/Import2.png)
Success! We see how many new titles are imported:
![Import step 3](images/Import3.png)
### Download your books -- DRM-free!
Automatically download some or all of your audible books. This shows you how much of your library is not yet downloaded and decrypted:
The stoplights will tell you a title's status:
* Green: downloaded and decrypted
* Yellow: downloaded but still encrypted with DRM
* Red: not downloaded
* PDF icon without arrow: downloaded
* PDF with arrow: not downloaded
Or hover over the button to see the status.
![Liberate book step 1](images/LiberateBook1.png)
Select Liberate > Begin Book Backups
You can also click on the stop light to download only that title and its PDF
![Liberate book step 2](images/LiberateBook2.png)
First the original book with DRM is downloaded
![Liberate book step 3](images/LiberateBook3.png)
Then it's decrypted so you can use it on any device you choose. The very first time you decrypt a book, this step will take a while. Every other book will go much faster. The first time, Libation has to figure out the special decryption key which allows your personal books to be unlocked.
![Liberate book step 4](images/LiberateBook4.png)
And voila! If you have multiple books not yet liberated, Libation will automatically move on to the next.
![Liberate book step 5](images/LiberateBook5.png)
The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
### Download PDF attachments
For books which include PDF downloads, Libation can download these for you as well and will attempt to store them with the book. "Book backup" will already download an available PDF. This additional option is useful when Audible adds a PDF to your book after you've already backed it up.
Select Liberate > Begin PDF Backups
![PDF download step 2](images/PdfDownload2.png)
The downloads work just like with books, only with no additional decryption needed.
![PDF download step 3](images/PdfDownload3.png)
### Details of downloaded files
![Post download](images/PostDownload.png)
When you set up Libation, you'll specify a Books directory. Libation looks inside that directory and all subdirectories to look for files or folders with each library book's audible id. This way, organization is completely up to you. When you download + decrypt a book, you get several files
* .m4b: your audiobook in m4b format. This is the most pure version of your audiobook and retains the highest quality. Now that it's decrypted, you can play it on any audio player and put it on any device. If you'd like, you can also use 3rd party tools to turn it into an mp3. The freedom to do what you want with your files was the original inspiration for Libation.
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
### Export your library
![Export](images/Export.png)
Export your library to Excel, CSV, or JSON
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/getting-started.md))
+3
View File
@@ -0,0 +1,3 @@
# This page has been moved to https://getlibation.com/docs/installation/linux
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/installation/linux.md))
+3
View File
@@ -0,0 +1,3 @@
# This page has been moved to https://getlibation.com/docs/installation/mac
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/installation/mac.md))
@@ -0,0 +1,3 @@
# This page has been moved to https://getlibation.com/docs/development/nix-linux-setup
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/development/nix-linux-setup.md))
+3
View File
@@ -0,0 +1,3 @@
# This page has been moved to https://getlibation.com/docs/features/naming-templates
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/features/naming-templates.md))
+2 -78
View File
@@ -1,79 +1,3 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
# This page has been moved to https://getlibation.com/docs/features/searching-and-filtering
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Searching and filtering: Table of Contents
- [Tags](#tags)
- [Searches](#searches)
- [Search examples](#search-examples)
- [Filters](#filters)
### Tags
To add tags to a title, click the tags button
![Tags step 1](images/Tags1.png)
Add as many tags as you'd like. Tags are separated by a space. Each tag can contain letters, numbers, and underscores
![Tags step 2](images/Tags2.png)
Tags are saved non-case specific for easy search. There is one special tag "hidden" which will also grey-out the book
![Tags step 3](images/Tags3.png)
To edit tags, just click the button again.
### Searches
Libation's advanced searching is built on the powerful Lucene search engine. Simple searches are effortless and powerful searches are simple. To search, just type and click Filter or press enter
* Type anything in the search box to search common fields: title, authors, narrators, and the book's audible id
* Use Lucene's "Query Parser Syntax" for advanced searching.
* Easy tutorial: http://www.lucenetutorial.com/lucene-query-syntax.html
* Full official guide: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html
* Tons of search fields, specific to audiobooks
* Synonyms so you don't have to memorize magic words. Eg: author and author**s** will both work
* Click [?] button for a full list of search fields and synonyms ![Filter options](images/FilterOptionsButton.png)
* Search by tag like \[this\]
* When tags have an underscore you can use part of the tag. This is useful for quick categories. The below examples make this more clear.
### Search examples
Search for anything with the word potter
![Search example: potter](images/SearchExamplePotter.png)
If you only want to see Harry Potter
![Search example: "harry potter"](images/SearchExampleHarryPotter.png)
If you only want to see potter except for Harry Potter. You can also use "-" instead of "NOT"
![Search example: "potter NOT harry"](images/SearchExamplePotterNotHarry.png)
![Search example: "potter -harry"](images/SearchExamplePotterNotHarry2.png)
To see only books written by Neil Gaiman where he also narrates his own book. (If you don't include AND, you'll see everything written by Neil Gaiman and also all books in your library which are self-narrated.)
![Search example: author:gaiman AND authornarrated](images/SearchExampleGaimanAuthorNarrated.png)
I tagged autobiographies as auto_bio and biographies written by someone else as bio. I can get only autobiographies with \[auto_bio\] or get both by searching \[bio\]
![Search example: \[bio\]](images/SearchExampleBio.png)
![Search example: \[auto_bio\]](images/SearchExampleAutoBio.png)
### Filters
If you have a search you want to save, click Add To Quick Filters to save it in your Quick Filters list. To use it again, select it from the Quick Filters list.
To edit this list go to Quick Filters > Edit quick filters. Here you can re-order the list, delete filters, double-click a filter to edit it, or double-click the bottom blank box to add a new filter.
Check "Quick Filters > Start Libation with 1st filter Default" to have your top filter automatically applied when Libation starts. In this top example, I want to always start without these: at books I've tagged hidden, books I've tagged as free_audible_originals, and books which I have rated.
![default filters](images/FiltersDefault.png)
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/features/searching-and-filtering.md))
Binary file not shown.
Binary file not shown.
Binary file not shown.
+32
View File
@@ -0,0 +1,32 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" width="512px" enable-background="new 0 0 512 512">
<path id="slosh" transform=
"translate(-50 23)
scale(0.7, 0.7)
rotate(12 256,256)"
d=
"M139,2
A 192,200 0 0 0 103,84
A 222,334 41 0 0 241,320
V478
H160
A 16,16 0 0 0 160,510
H352
A16 16 0 0 0 352,478
H271
V320
A 222,334 -41 0 0 409,84
A 192,200 0 0 0 373,2
M355,32
A 192,200 0 0 1 381,127
A 187.5,334 -35 0 1 256,286
A 187.5,334 35 0 1 131,127
A 192,200 0 0 1 157,32
H355
M146,147
A 168,300 35 0 0 256,270
A 168,300 -35 0 0 366,128
S 360,50 280,110
S 192,128 147,147
z" />
<use href="#slosh" transform="translate(512 0) scale(-1 1)" />
</svg>

After

Width:  |  Height:  |  Size: 736 B

+41
View File
@@ -0,0 +1,41 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 288" enable-background="new 0 0 288 288">
<defs>
<g id="glass">
<path transform="translate(16 16)" fill-rule="evenodd" d=
"M177,16
H79
A 32.0781 63.7932 -1.5106 0 0 66 80
A 158.789 471.1259 41.9466 0 0 90 131
A 81.7197 122.0515 35.3745 0 0 128 143.3484
A 81.7197 122.0515 -35.3745 0 0 166 131
A 158.789 471.1259 -41.9466 0 0 190 80
A 32.0781 63.7932 1.5106 0 0 177 16
L 184 0
A 44.7901 78.5247 1.1521 0 1 194 122
A 97.0039 135.3148 -36.2124 0 1 136 159
V 240
H 176
A 8 8 0 0 1 176 256
H 80
A 8 8 0 0 1 80 240
H 120
V 159
A 97.0039 135.3148 36.2124 0 1 62 122
A 44.7901 78.5247 -1.1521 0 1 72 0
H184
z"/>
</g>
<g transform="translate(16 16)" id="wine-level">
<path d=
"M182,64
H 74
A 115.9979 308.8033 38.9474 0 0 128 134.4277
A 115.9979 308.8033 -38.9474 0 0 182,64
z"/>
</g>
</defs>
<use href="#glass" stroke="#ffffffa0" stroke-width="16" fill="Transparent" />
<use href="#wine-level" stroke="#ffffffa0" stroke-width="16" fill="Transparent" />
<use href="#glass" fill="Black" />
<use href="#wine-level" fill="Black" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+31
View File
@@ -0,0 +1,31 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 288 288" enable-background="new 0 0 288 288">
<g>
<path transform="rotate(90 128,128) translate(60 -16)" fill-rule="evenodd" d=
"M177,16
H79
A 32.0781 63.7932 -1.5106 0 0 66 80
A 158.789 471.1259 41.9466 0 0 90 131
A 81.7197 122.0515 35.3745 0 0 128 143.3484
A 81.7197 122.0515 -35.3745 0 0 166 131
A 158.789 471.1259 -41.9466 0 0 190 80
A 32.0781 63.7932 1.5106 0 0 177 16
L 184 0
A 44.7901 78.5247 1.1521 0 1 194 122
A 97.0039 135.3148 -36.2124 0 1 136 159
V 240
H 176
A 8 8 0 0 1 176 256
H 80
A 8 8 0 0 1 80 240
H 120
V 159
A 97.0039 135.3148 36.2124 0 1 62 122
A 44.7901 78.5247 -1.1521 0 1 72 0
H184
M170,115
V24
A 19.5181 45.9183 -3.3549 0 1 182.4322 69.5
A 19.5181 45.9183 3.3549 0 1 170 115
z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 936 B

+33
View File
@@ -0,0 +1,33 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
<path
transform=
"rotate(15 256,256)
translate(0 25)
scale(0.93, 0.93)"
d=
"M139,2
A 192,200 0 0 0 103,84
A 222,334 41 0 0 241,320
V478
H160
A 16,16 0 0 0 160,510
H352
A16 16 0 0 0 352,478
H271
V320
A 222,334 -41 0 0 409,84
A 192,200 0 0 0 373,2
M355,32
A 192,200 0 0 1 381,127
A 187.5,334 -35 0 1 256,286
A 187.5,334 35 0 1 131,127
A 192,200 0 0 1 157,32
H355
M146,147
A 168,300 35 0 0 256,270
A 168,300 -35 0 0 366,128
S 360,50 280,110
S 192,128 147,147
z" />
</svg>

After

Width:  |  Height:  |  Size: 649 B

+40 -51
View File
@@ -1,69 +1,58 @@
# Libation: Liberate your Library
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
**Libation** is a free, open-source application for downloading and managing your Audible audiobooks. It decrypts your library, removes DRM, and lets you own your audiobooks forever.
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
> <a href="https://getlibation.com"><img src=".github/download-icon.svg" width="20" height="20" alt="" /></a> **[Which version should I download?](https://getlibation.com)** — get a recommended download for your system on our site.
# Table of Contents
## Features
- [Audible audiobook manager](#audible-audiobook-manager)
- [The good](#the-good)
- [The bad](#the-bad)
- [The ugly](#the-ugly)
- [Getting started](Documentation/GettingStarted.md)
- [Download Libation](Documentation/GettingStarted.md#download-libation-1)
- [Installation](Documentation/GettingStarted.md#installation)
- [Create Accounts](Documentation/GettingStarted.md#create-accounts)
- [Import your library](Documentation/GettingStarted.md#import-your-library)
- [Download your books -- DRM-free!](Documentation/GettingStarted.md#download-your-books----drm-free)
- [Download PDF attachments](Documentation/GettingStarted.md#download-pdf-attachments)
- [Details of downloaded files](Documentation/GettingStarted.md#details-of-downloaded-files)
- [Export your library](Documentation/GettingStarted.md#export-your-library)
- [Searching and filtering](Documentation/SearchingAndFiltering.md)
- [Tags](Documentation/SearchingAndFiltering.md#tags)
- [Searches](Documentation/SearchingAndFiltering.md#searches)
- [Search examples](Documentation/SearchingAndFiltering.md#search-examples)
- [Filters](Documentation/SearchingAndFiltering.md#filters)
- [Advanced](Documentation/Advanced.md)
- [Files and folders](Documentation/Advanced.md#files-and-folders)
- [Linux and Mac (unofficial)](Documentation/Advanced.md#linux-and-mac)
- [Settings](Documentation/Advanced.md#settings)
- [Custom File Naming](Documentation/Advanced.md#custom-file-naming)
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
- **Unlock Your Library**: Download and remove DRM from your audiobooks.
- **Cross-Platform**: Fully supported on Windows, macOS, and Linux.
- **Region Support**: Works with Audible regions US, UK, Canada, Germany, France, Australia, Japan, India, and Spain.
- **Advanced Organization**: Search, filter, and tag your books.
- **Fast & Efficient**: Powered by AAXClean for fast decryption without heavy dependencies like ffmpeg.
- **Import**: Easily import your existing library, including cover art.
## Getting started
## Getting started with Libation
All documentation has been moved to our new site: [getlibation.com](https://getlibation.com). Or jump to the important bits:
* [Getting Started](https://getlibation.com/docs/getting-started)
* [Download](https://github.com/rmcrackan/Libation/releases/latest)
* [Step-by-step walk-through](Documentation/GettingStarted.md)
* [Issues, bugs, and requests](https://github.com/rmcrackan/Libation/issues)
* [Documentation](https://getlibation.com/docs/index)
## Audible audiobook manager
## Development
### The good
Grab the latest release for your platform from the [Releases Page](https://github.com/rmcrackan/Libation/releases/latest).
* Import library from audible, including cover art
* Download and remove DRM from all books
* Download accompanying PDFs
* Add tags to books for better organization
* Powerful advanced search built on the Lucene search engine
* Customizable saved filters for common searches
* Open source
* Supports most regions: US, UK, Canada, Germany, France, Australia, Japan, India, and Spain
## Documentation
<a name="theBad"/>
Comprehensive documentation is available in the `docs` directory and on our [Documentation Site](https://getlibation.com/docs).
### The bad
- [Getting Started](https://getlibation.com/docs/getting-started)
- [FAQ](https://getlibation.com/docs/frequently-asked-questions)
- [Spatial Audio & DRM](https://getlibation.com/docs/advanced/spatial-audio)
* Windows only
* Large file size
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows
## Development & Contributing
### The ugly
We welcome contributions!
* Documentation? Yer lookin' at it
* This is a single-developer personal passion project. Support, response, updates, enhancements, bug fixes etc are as my free time allows
* I have a full-time job, a life, and a finite attention span. Therefore a lot of time can potentially go by with no improvements of any kind
- **[Development Getting Started](https://getlibation.com/docs/development/getting-started)**: Setup your environment.
- **[Contribute](https://getlibation.com/docs/development/contribute)**: How to contribute code.
- **[Website & Docs](https://getlibation.com/docs/development/website)**: How to run and improve the documentation.
- **[Linux Setup (Nix)](https://getlibation.com/docs/development/nix-linux-setup)**: Nix-based environment setup.
Disclaimer: I've made every good-faith effort to include nothing insecure, malicious, anti-privacy, or destructive. That said: use at your own risk.
## Community & Support
I made this for myself and I want to share it with the great programming and audible/audiobook communiites which have been so generous with their time and help.
- **[Issues](https://github.com/rmcrackan/Libation/issues)**: Report bugs or request features.
- **[PayPal](https://paypal.me/mcrackan?locale.x=en_us)**: Support the project if you find it useful.
## License
Libation is released under the GPL-3.0 License
---
If you found this useful, tell a friend. If you found this REALLY useful, you can click here to PayPal.me
...or just tell more friends. As long as I'm maintaining this software, it will remain free and open source.
+130
View File
@@ -0,0 +1,130 @@
#!/bin/bash
BIN_DIR=$1; shift
VERSION=$1; shift
ARCH=$1; shift
if [ -z "$BIN_DIR" ]
then
echo "This script must be called with a the Libation Linux bins directory as an argument."
exit
fi
if [ ! -d "$BIN_DIR" ]
then
echo "The directory \"$BIN_DIR\" does not exist."
exit
fi
if [ -z "$VERSION" ]
then
echo "This script must be called with the Libation version number as an argument."
exit
fi
if [ -z "$ARCH" ]
then
echo "This script must be called with the Libation cpu architecture as an argument."
exit
fi
ARCH=$(echo $ARCH | sed 's/x64/amd64/')
DEB_DIR=./deb
FOLDER_EXEC=$DEB_DIR/usr/lib/libation
echo "Exec dir: $FOLDER_EXEC"
mkdir -p $FOLDER_EXEC
echo "Moving bins from $BIN_DIR to $FOLDER_EXEC"
mv "${BIN_DIR}/"* $FOLDER_EXEC
if [ $? -ne 0 ]
then echo "Error moving ${BIN_DIR} files"
exit
fi
delfiles=('LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
for n in "${delfiles[@]}"
do
echo "Deleting $n"
rm $FOLDER_EXEC/$n
done
FOLDER_ICON=$DEB_DIR/usr/share/icons/hicolor/scalable/apps/
echo "Icon dir: $FOLDER_ICON"
FOLDER_DESKTOP=$DEB_DIR/usr/share/applications
echo "Desktop dir: $FOLDER_DESKTOP"
FOLDER_DEBIAN=$DEB_DIR/DEBIAN
echo "Debian dir: $FOLDER_DEBIAN"
mkdir -p $FOLDER_ICON
mkdir -p $FOLDER_DESKTOP
mkdir -p $FOLDER_DEBIAN
echo "Copying icon..."
cp $FOLDER_EXEC/libation_glass.svg $FOLDER_ICON/libation.svg
echo "Copying desktop file..."
cp $FOLDER_EXEC/Libation.desktop $FOLDER_DESKTOP/Libation.desktop
echo "Creating pre-install file..."
echo "#!/bin/bash
# Pre-install script, removes previous installation program files and sym links
echo \"Removing previously created symlinks...\"
rm /usr/bin/libation
rm /usr/bin/hangover
rm /usr/bin/libationcli
echo \"Removing previously installed Libation files...\"
rm -r /usr/lib/libation
# making sure it won't stop installation
exit 0
" >> $FOLDER_DEBIAN/preinst
echo "Creating post-install file..."
echo "#!/bin/bash
gtk-update-icon-cache -f /usr/share/icons/hicolor/
ln -s /usr/lib/libation/Libation /usr/bin/libation
ln -s /usr/lib/libation/Hangover /usr/bin/hangover
ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
# Increase the maximum number of inotify instances
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
fi
" >> $FOLDER_DEBIAN/postinst
echo "Creating control file..."
echo "Package: Libation
Version: $VERSION
Architecture: $ARCH
Essential: no
Priority: optional
Maintainer: github.com/rmcrackan
Description: liberate your audiobooks
Recommends: libgtk-3-0, libwebkit2gtk-4.1-0
" >> $FOLDER_DEBIAN/control
echo "Changing permissions for pre- and post-install files..."
chmod +x "$FOLDER_DEBIAN/preinst"
chmod +x "$FOLDER_DEBIAN/postinst"
if [ "$(uname -s)" == "Darwin" ]; then
echo "macOS detected, installing dpkg"
brew install dpkg
fi
DEB_FILE=Libation.${VERSION}-linux-chardonnay-${ARCH}.deb
echo "Creating $DEB_FILE"
dpkg-deb -Zxz --build $DEB_DIR ./$DEB_FILE
echo "moving to ./bundle/$DEB_FILE"
mkdir bundle
mv $DEB_FILE ./bundle/$DEB_FILE
rm -r "$BIN_DIR"
echo "Done!"
+171
View File
@@ -0,0 +1,171 @@
#!/bin/bash
set -euo pipefail
BIN_DIR=$1; shift
VERSION=$1; shift
ARCH=$1; shift
SIGN_WITH_KEY=$1; shift
if [ -z "$BIN_DIR" ]
then
echo "This script must be called with a the Libation macos bins directory as an argument."
exit 1
fi
if [ ! -d "$BIN_DIR" ]
then
echo "The directory \"$BIN_DIR\" does not exist."
exit 1
fi
if [ -z "${VERSION:-}" ]
then
echo "This script must be called with the Libation version number as an argument."
exit 1
fi
if [ -z "${ARCH:-}" ]
then
echo "This script must be called with the Libation cpu architecture as an argument."
exit 1
fi
if [ "$SIGN_WITH_KEY" != "true" ]
then
echo "::warning:: App will fail Gatekeeper verification without valid Apple Team information."
fi
BUNDLE=./Libation.app
echo "Bundle dir: $BUNDLE"
if [[ -d $BUNDLE ]]
then
echo "$BUNDLE directory already exists, aborting."
exit 1
fi
# hdiutil create intermittently fails on CI with "Resource busy" (macOS background scanning).
create_dmg_with_retry() {
local src_folder="$1"
local output_path="$2"
local max_attempts=10
local attempt=1
local delay=3
while [ "$attempt" -le "$max_attempts" ]; do
echo "Creating disk image (attempt ${attempt}/${max_attempts}): ${output_path}"
rm -f "${output_path}"
sync
sleep 1
if hdiutil create -srcFolder "${src_folder}" -ov -format UDZO -o "${output_path}"; then
if [ -f "${output_path}" ]; then
return 0
fi
fi
echo "hdiutil create failed on attempt ${attempt}"
hdiutil detach -force /Volumes/Libation 2>/dev/null || true
rm -f "${output_path}" "${output_path}.temp.dmg" 2>/dev/null || true
sync
if [ "$attempt" -lt "$max_attempts" ]; then
echo "Waiting ${delay}s before retry..."
sleep "${delay}"
delay=$((delay + 2))
fi
attempt=$((attempt + 1))
done
echo "Error: hdiutil create failed after ${max_attempts} attempts"
return 1
}
BUNDLE_CONTENTS=$BUNDLE/Contents
echo "Bundle Contents dir: $BUNDLE_CONTENTS"
BUNDLE_RESOURCES=$BUNDLE_CONTENTS/Resources
echo "Resources dir: $BUNDLE_RESOURCES"
BUNDLE_MACOS=$BUNDLE_CONTENTS/MacOS
echo "MacOS dir: $BUNDLE_MACOS"
mkdir -p $BUNDLE_CONTENTS
mkdir -p $BUNDLE_RESOURCES
mkdir -p $BUNDLE_MACOS
mv "${BIN_DIR}/"* $BUNDLE_MACOS
if [ $? -ne 0 ]; then
echo "Error moving ${BIN_DIR} files"
exit 1
fi
echo "Make fileicon executable..."
chmod +x $BUNDLE_MACOS/fileicon
echo "Moving icon..."
mv $BUNDLE_MACOS/libation.icns $BUNDLE_RESOURCES/libation.icns
echo "Moving Info.plist file..."
mv $BUNDLE_MACOS/Info.plist $BUNDLE_CONTENTS/Info.plist
echo "Moving Libation_DS_Store file..."
mv $BUNDLE_MACOS/Libation_DS_Store ./Libation_DS_Store
echo "Moving background.png file..."
mv $BUNDLE_MACOS/background.png ./background.png
echo "Moving background.png file..."
mv $BUNDLE_MACOS/Libation.entitlements ./Libation.entitlements
PLIST_ARCH=$(echo $ARCH | sed 's/x64/x86_64/')
echo "Set LSArchitecturePriority to $PLIST_ARCH"
# Portable sed -i (BSD sed on macOS requires backup arg; use .bak then remove)
sed -i.bak "s/ARCHITECTURE_STRING/$PLIST_ARCH/" $BUNDLE_CONTENTS/Info.plist && rm -f $BUNDLE_CONTENTS/Info.plist.bak
echo "Set CFBundleVersion to $VERSION"
sed -i.bak "s/VERSION_STRING/$VERSION/" $BUNDLE_CONTENTS/Info.plist && rm -f $BUNDLE_CONTENTS/Info.plist.bak
delfiles=('MacOSConfigApp' 'MacOSConfigApp.deps.json' 'MacOSConfigApp.runtimeconfig.json')
for n in "${delfiles[@]}"
do
echo "Deleting $n"
rm $BUNDLE_MACOS/$n
done
DMG_FILE="Libation.${VERSION}-macOS-chardonnay-${ARCH}.dmg"
all_identities=$(security find-identity -v -p codesigning)
identity=$(echo ${all_identities} | sed -n 's/.*"\(.*\)".*/\1/p')
if [ "$SIGN_WITH_KEY" == "true" ]; then
echo "Signing executables in: $BUNDLE"
codesign --force --deep --timestamp --options=runtime --entitlements "./Libation.entitlements" --sign "${identity}" "$BUNDLE"
codesign --verify --verbose "$BUNDLE"
else
echo "Signing with empty key: $BUNDLE"
codesign --force --deep -s - $BUNDLE
fi
echo "Creating app disk image: $DMG_FILE"
mkdir Libation
mv $BUNDLE ./Libation/$BUNDLE
mv Libation_DS_Store Libation/.DS_Store
mkdir Libation/.background
mv background.png Libation/.background/
ln -s /Applications "./Libation/ "
mkdir ./bundle
create_dmg_with_retry ./Libation "./bundle/$DMG_FILE"
# Create a .DS_Store by:
# - mounting an existing image in shadow mode (hdiutil attach Libation.dmg -shadow junk.dmg)
# - Open the folder and edit it to your liking.
# - Copy the .DS_Store from the directory and save it to Libation_DS_Store
if [ "$SIGN_WITH_KEY" == "true" ]; then
echo "Signing $DMG_FILE"
codesign --deep --sign "${identity}" "./bundle/$DMG_FILE"
fi
echo "Done!"
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
BIN_DIR=$1; shift
VERSION=$1; shift
ARCH=$1; shift
if [ -z "$BIN_DIR" ]
then
echo "This script must be called with a the Libation Linux bins directory as an argument."
exit
fi
if [ ! -d "$BIN_DIR" ]
then
echo "The directory \"$BIN_DIR\" does not exist."
exit
fi
if [ -z "$VERSION" ]
then
echo "This script must be called with the Libation version number as an argument."
exit
fi
if [ -z "$ARCH" ]
then
echo "This script must be called with the Libation cpu architecture as an argument."
exit
fi
BASEDIR=$(pwd)
delfiles=('LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
if [[ "$ARCH" == "x64" ]]
then
ARCH_RPM="x86_64"
ARCH="amd64"
else
ARCH_RPM="aarch64"
fi
notinstalled=('libcoreclrtraceptprovider.so' 'libation_glass.svg' 'Libation.desktop')
mkdir -p ~/rpmbuild/SPECS
mkdir ~/rpmbuild/BUILD
mkdir ~/rpmbuild/RPMS
echo "Name: libation
Version: ${VERSION}
Release: 1
Summary: Liberate your Audible Library
License: GPLv3+
URL: https://github.com/rmcrackan/Libation
Source0: https://github.com/rmcrackan/Libation
Requires: bash gtk3 webkit2gtk4.1
%define __os_install_post %{nil}
%description
Liberate your Audible Library
%install
mkdir -p %{buildroot}%{_libdir}/%{name}
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
mkdir -p %{buildroot}%{_datadir}/applications
if test -f 'libcoreclrtraceptprovider.so'; then
rm 'libcoreclrtraceptprovider.so'
fi
install -m 666 libation_glass.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/libation.svg
install -m 666 Libation.desktop %{buildroot}%{_datadir}/applications/Libation.desktop
rm libation_glass.svg
rm Libation.desktop
install * %{buildroot}%{_libdir}/%{name}/
%post
if [ \$1 -eq 1 ] ; then
# Initial installation
ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation
ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover
ln -s %{_libdir}/%{name}/LibationCli %{_bindir}/libationcli
gtk-update-icon-cache -f %{_datadir}/icons/hicolor/
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
fi
fi
%postun
if [ \$1 -eq 0 ] ; then
# Uninstall
rm %{_bindir}/libation
rm %{_bindir}/hangover
rm %{_bindir}/libationcli
fi
%files
%{_datadir}/icons/hicolor/scalable/apps/libation.svg
%{_datadir}/applications/Libation.desktop" >> ~/rpmbuild/SPECS/libation.spec
cd "$BIN_DIR"
for f in *; do
if [[ " ${delfiles[*]} " =~ " ${f} " ]]; then
echo "Deleting $f"
elif [[ ! " ${notinstalled[*]} " =~ " ${f} " ]]; then
echo "%{_libdir}/%{name}/${f}" >> ~/rpmbuild/SPECS/libation.spec
cp $f ~/rpmbuild/BUILD/
else
cp $f ~/rpmbuild/BUILD/
fi
done
cd ~/rpmbuild/SPECS/
rpmbuild -bb --target $ARCH_RPM libation.spec
cd $BASEDIR
RPM_FILE=$(ls ~/rpmbuild/RPMS/${ARCH_RPM})
mkdir bundle
mv ~/rpmbuild/RPMS/${ARCH_RPM}/$RPM_FILE "./bundle/Libation.${VERSION}-linux-chardonnay-${ARCH}.rpm"
+164
View File
@@ -0,0 +1,164 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Builds a Libation Windows setup.exe from a published bin directory using Inno Setup.
.DESCRIPTION
Optionally runs dotnet publish (same projects as CI), removes standalone WindowsConfigApp
artifacts, then compiles Scripts/Windows/Libation.iss.
.PARAMETER Version
Libation version string (e.g. 13.4.1).
.PARAMETER Ui
Avalonia (Chardonnay) or WinForms (Classic). Classic is x64 only.
.PARAMETER Architecture
x64 or arm64 (Avalonia only).
.PARAMETER BinDir
Folder containing published files. Defaults to repo-root bin.
.PARAMETER SkipPublish
Skip dotnet publish; BinDir must already contain a complete publish output.
.EXAMPLE
.\Build-WindowsInstaller.ps1 -Version 13.4.1 -Ui Avalonia -Architecture x64
.EXAMPLE
.\Build-WindowsInstaller.ps1 -Version 13.4.1 -Ui WinForms
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $Version,
[ValidateSet('Avalonia', 'WinForms')]
[string] $Ui = 'Avalonia',
[ValidateSet('x64', 'arm64')]
[string] $Architecture = 'x64',
[string] $BinDir = '',
[switch] $SkipPublish
)
$ErrorActionPreference = 'Stop'
if ($Ui -eq 'WinForms' -and $Architecture -ne 'x64') {
throw 'Classic (WinForms) is only published for x64.'
}
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
$sourceDir = Join-Path $repoRoot 'Source'
if (-not $BinDir) {
$BinDir = Join-Path $repoRoot 'bin'
}
if (-not (Test-Path $BinDir)) {
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
}
$BinDir = (Resolve-Path $BinDir).Path
$runtime = "win-$Architecture"
$releaseName = if ($Ui -eq 'WinForms') { 'classic' } else { 'chardonnay' }
$outputBase = if ($Ui -eq 'WinForms') {
"Libation-Classic.$Version-windows-$releaseName-$Architecture-setup"
} else {
"Libation.$Version-windows-$releaseName-$Architecture-setup"
}
$architecturesAllowed = if ($Architecture -eq 'arm64') { 'arm64' } else { 'x64compatible' }
function Find-ISCC {
$candidates = @(
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
"${env:LocalAppData}\Programs\Inno Setup 6\ISCC.exe"
)
foreach ($path in $candidates) {
if (Test-Path $path) { return $path }
}
return $null
}
if (-not $SkipPublish -and (Test-Path $BinDir)) {
Write-Host "Clearing $BinDir before publish ..."
Get-ChildItem -LiteralPath $BinDir -Force | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
if (-not $SkipPublish) {
Write-Host "Publishing Libation ($Ui, $runtime) to $BinDir ..."
Push-Location $sourceDir
try {
$publishArgs = @(
'--runtime', $runtime,
'--configuration', 'Release',
'--output', $BinDir,
'-p:PublishProtocol=FileSystem',
'-p:PublishReadyToRun=false',
'-p:SelfContained=true'
)
dotnet publish "Libation$Ui/Libation$Ui.csproj" @publishArgs
dotnet publish 'LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj' @publishArgs
dotnet publish 'LibationCli/LibationCli.csproj' @publishArgs
dotnet publish "Hangover$Ui/Hangover$Ui.csproj" @publishArgs
}
finally {
Pop-Location
}
}
Write-Host 'Removing standalone WindowsConfigApp launcher artifacts ...'
$removeFiles = @(
'WindowsConfigApp.exe',
'WindowsConfigApp.runtimeconfig.json',
'WindowsConfigApp.deps.json'
)
Push-Location $BinDir
try {
foreach ($file in $removeFiles) {
if (Test-Path $file) { Remove-Item $file -Force }
}
if (-not (Test-Path 'Libation.exe')) {
throw "Libation.exe not found in $BinDir. Publish or point -BinDir at a complete output folder."
}
if (-not (Test-Path 'ZipExtractor.exe')) {
throw "ZipExtractor.exe not found in $BinDir. It is required for in-app upgrades."
}
}
finally {
Pop-Location
}
$iscc = Find-ISCC
if (-not $iscc) {
throw @"
Inno Setup 6 (ISCC.exe) was not found. Install from https://jrsoftware.org/isdl.php
and ensure ISCC.exe is under Program Files (x86)\Inno Setup 6\ or similar.
"@
}
$setupIcon = Join-Path $PSScriptRoot 'libation.ico'
if (-not (Test-Path $setupIcon)) {
throw "Installer icon not found: $setupIcon"
}
$iss = Join-Path $PSScriptRoot 'Libation.iss'
$defines = @(
"/DMyVersion=$Version",
"/DMySetupIcon=$setupIcon",
"/DMyReleaseName=$releaseName",
"/DMyArchitecture=$Architecture",
"/DOutputBaseFilename=$outputBase",
"/DSourceDir=$BinDir",
"/DOutputDir=$BinDir",
"/DArchitecturesAllowed=$architecturesAllowed"
)
Write-Host "Compiling $outputBase.exe ..."
& $iscc $defines $iss
if ($LASTEXITCODE -ne 0) {
throw "ISCC failed with exit code $LASTEXITCODE."
}
$setupPath = Join-Path $BinDir "$outputBase.exe"
Write-Host "Done: $setupPath"
+93
View File
@@ -0,0 +1,93 @@
; Libation Windows installer (Inno Setup 6.x)
;
; Compile with ISCC and /D defines. See Scripts/Windows/README.md and
; Build-WindowsInstaller.ps1 for local builds; CI will pass the same defines.
;
; Required /D defines from build script:
; MyVersion, MyReleaseName, MyArchitecture, MySetupIcon,
; OutputBaseFilename, SourceDir, OutputDir, ArchitecturesAllowed
#ifndef MyVersion
#define MyVersion "0.0.0-dev"
#endif
#ifndef MyReleaseName
#define MyReleaseName "chardonnay"
#endif
#ifndef MyArchitecture
#define MyArchitecture "x64"
#endif
#ifndef MySetupIcon
#define MySetupIcon "libation.ico"
#endif
#ifndef OutputBaseFilename
#define OutputBaseFilename "Libation.0.0.0-dev-windows-chardonnay-x64-setup"
#endif
#ifndef SourceDir
#define SourceDir "."
#endif
#ifndef OutputDir
#define OutputDir "."
#endif
#ifndef ArchitecturesAllowed
#define ArchitecturesAllowed "x64compatible"
#endif
#if MyReleaseName == 'classic'
#define InstallFolderName "Libation-Classic"
#define AppName "Libation (Classic)"
#else
#define InstallFolderName "Libation"
#define AppName "Libation (Chardonnay)"
#endif
[Setup]
#if MyReleaseName == 'classic'
AppId={{B1C2D3E4-F5A6-4B7C-8D9E-0F1A2B3C4D5E}}
#elif MyArchitecture == 'arm64'
AppId={{C2D3E4F5-A6B7-4C8D-9E0F-1A2B3C4D5E6F}}
#else
AppId={{A7B8C9D0-E1F2-4A3B-8C9D-0E1F2A3B4C5D}}
#endif
AppName={#AppName}
AppVersion={#MyVersion}
AppVerName={#AppName} {#MyVersion}
AppPublisher=Libation
AppPublisherURL=https://getlibation.com/
AppSupportURL=https://github.com/rmcrackan/Libation/issues
AppUpdatesURL=https://github.com/rmcrackan/Libation/releases
DefaultDirName={localappdata}\{#InstallFolderName}
DefaultGroupName={#AppName}
PrivilegesRequired=lowest
OutputDir={#OutputDir}
OutputBaseFilename={#OutputBaseFilename}
SourceDir={#SourceDir}
ArchitecturesAllowed={#ArchitecturesAllowed}
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=no
Compression=lzma2
SolidCompression=yes
WizardStyle=modern
SetupIconFile={#MySetupIcon}
UninstallDisplayIcon={app}\Libation.exe
UninstallDisplayName={#AppName}
VersionInfoVersion={#MyVersion}
VersionInfoProductName={#AppName}
VersionInfoCompany=Libation
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\Libation"; Filename: "{app}\Libation.exe"; Comment: "Libation audiobook manager"
Name: "{group}\Hangover"; Filename: "{app}\Hangover.exe"; Comment: "Hangover license viewer"
Name: "{group}\Libation CLI"; Filename: "{app}\LibationCli.exe"; Comment: "Libation command-line tools"
Name: "{autodesktop}\Libation"; Filename: "{app}\Libation.exe"; Tasks: desktopicon; Comment: "Libation audiobook manager"
[Run]
Filename: "{app}\Libation.exe"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
+89
View File
@@ -0,0 +1,89 @@
# Windows installer (Inno Setup)
Step 8.1 of the Windows installers plan: a parameterized Inno Setup script that installs the same flat publish layout as the release zip, under a per-user directory, so the in-app upgrader can still overlay files via `ZipExtractor.exe`.
## Prerequisites
1. [.NET SDK](https://dotnet.microsoft.com/download) (same version as the repo; see `build.yml`).
2. [Inno Setup 6](https://jrsoftware.org/isdl.php) with `ISCC.exe` on PATH or in a default install location.
## Quick build (publish + installer)
From PowerShell, repo root or this folder:
```powershell
.\Scripts\Windows\Build-WindowsInstaller.ps1 -Version 13.4.1 -Ui Avalonia -Architecture x64
```
Outputs:
- Publish tree: `bin\` (repo root)
- Installer: `bin\Libation.13.4.1-windows-chardonnay-x64-setup.exe`
### Variants
| UI | `-Ui` | `-Architecture` | Install folder | Output prefix |
|----|-------|-----------------|----------------|---------------|
| Chardonnay x64 | `Avalonia` | `x64` | `%LocalAppData%\Libation` | `Libation.*` |
| Chardonnay arm64 | `Avalonia` | `arm64` | `%LocalAppData%\Libation` | `Libation.*` |
| Classic x64 | `WinForms` | `x64` (default) | `%LocalAppData%\Libation-Classic` | `Libation-Classic.*-setup.exe` |
Examples:
```powershell
.\Scripts\Windows\Build-WindowsInstaller.ps1 -Version 13.4.1 -Ui WinForms
.\Scripts\Windows\Build-WindowsInstaller.ps1 -Version 13.4.1 -Ui Avalonia -Architecture arm64
```
### Reuse an existing publish folder
```powershell
.\Scripts\Windows\Build-WindowsInstaller.ps1 -Version 13.4.1 -Ui Avalonia -BinDir C:\path\to\bin -SkipPublish
```
`BinDir` must already match CI layout (including `ZipExtractor.exe`, without standalone `WindowsConfigApp.exe`).
## Manual ISCC (without the helper script)
After publish and cleanup (remove `WindowsConfigApp.exe`, `WindowsConfigApp.runtimeconfig.json`, `WindowsConfigApp.deps.json` from `bin`), from `Scripts\Windows`:
```text
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" Libation.iss ^
/DMyVersion=13.4.1 ^
/DMyPrefix= ^
/DMyReleaseName=chardonnay ^
/DMyArchitecture=x64 ^
/DInstallFolderName=Libation ^
/DAppName=Libation ^
/DOutputBaseFilename=Libation.13.4.1-windows-chardonnay-x64-setup ^
/DSourceDir=C:\Dev\_rmcrackan\Libation\bin ^
/DOutputDir=C:\Dev\_rmcrackan\Libation\bin ^
/DArchitecturesAllowed=x64compatible
```
Classic x64: use `MyReleaseName=classic` and matching `OutputBaseFilename` (`Libation-Classic.{version}-windows-classic-x64-setup`). Install folder and display name are set in `Libation.iss`. GitHub zip assets use `Libation-Classic.{version}-windows-classic-x64.zip`; `.releaseindex.json` also accepts legacy `Classic-Libation.*` zips on older releases.
arm64: set `MyArchitecture=arm64` and `ArchitecturesAllowed=arm64`.
## Installer behavior
- **Per-user install** (`PrivilegesRequired=lowest`, default dir under `{localappdata}`).
- **Shortcuts** target `{app}\Libation.exe` (required for upgrade path detection).
- **Does not** create `appsettings.json` or Libation Files; same as zip install.
- **Includes** full publish output so `ZipExtractor.exe` remains for in-app updates.
- **Settings -> Apps:** shows `Libation (Chardonnay)` or `Libation (Classic)` with version in `DisplayVersion` (Inno at install; synced after in-app zip upgrades via `WindowsUninstallRegistrySync`); icon from `SetupIconFile` / `UninstallDisplayIcon` (`Scripts/Windows/libation.ico` and `{app}\Libation.exe`).
## Next plan step (8.2)
Install via `*-setup.exe`, run the app, then verify an older release can upgrade in-app using the existing zip-based upgrader.
CI wiring (`build-windows.yml`) is step 8.3.
## Files in this folder
| File | Purpose |
|------|---------|
| `Libation.iss` | Parameterized Inno Setup script |
| `libation.ico` | Setup/uninstaller icon (copied from Avalonia assets) |
| `Build-WindowsInstaller.ps1` | Local publish + ISCC helper |
| `README.md` | This document |
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+14 -6
View File
@@ -1,15 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean" Version="0.4.7" />
<PackageReference Include="AAXClean.Codecs" Version="0.2.7" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="3.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FileManager\FileManager.csproj" />
</ItemGroup>
+134 -104
View File
@@ -1,120 +1,150 @@
using System;
using AAXClean;
using Dinah.Core.Net.Http;
using AAXClean;
using Mpeg4Lib;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace AaxDecrypter
namespace AaxDecrypter;
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
{
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
public event EventHandler<MetadataItems>? RetrievedMetadata;
public Mp4File? AaxFile { get; private set; }
protected Mp4Operation? AaxConversion { get; set; }
protected AaxcDownloadConvertBase(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
: base(outDirectory, cacheDirectory, dlOptions) { }
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
public override void SetCoverArt(byte[] coverArt)
{
public event EventHandler<AppleTags> RetrievedMetadata;
base.SetCoverArt(coverArt);
if (coverArt is not null && AaxFile?.MetadataItems is not null)
AaxFile.MetadataItems.Cover = coverArt;
}
protected AaxFile AaxFile;
public override async Task CancelAsync()
{
await base.CancelAsync();
await (AaxConversion?.CancelAsync() ?? Task.CompletedTask);
}
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic) { }
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
public override void SetCoverArt(byte[] coverArt)
private Mp4File Open()
{
if (DownloadOptions.DecryptionKeys is not KeyData[] keys || keys.Length == 0)
throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} cannot be null or empty for a '{DownloadOptions.InputType}' file.");
else if (DownloadOptions.InputType is FileType.Dash)
{
base.SetCoverArt(coverArt);
if (coverArt is not null && AaxFile?.AppleTags is not null)
AaxFile.AppleTags.Cover = coverArt;
//We may have multiple keys , so use the key whose key ID matches
//the dash files default Key ID.
var keyIds = keys.Select(k => new Guid(k.KeyPart1, bigEndian: true)).ToArray();
var dash = new DashFile(InputFileStream);
if (dash.Tenc is null)
throw new InvalidOperationException("The DASH file does not contain 'tenc' box, indicating that it is unencrypted.");
var kidIndex = Array.IndexOf(keyIds, dash.Tenc.DefaultKID);
if (kidIndex == -1)
throw new InvalidOperationException($"None of the {keyIds.Length} key IDs match the dash file's default KeyID of {dash.Tenc.DefaultKID}");
keys[0] = keys[kidIndex];
var keyId = keys[kidIndex].KeyPart1;
var key = keys[kidIndex].KeyPart2 ?? throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} for '{DownloadOptions.InputType}' must have a non-null decryption key (KeyPart2).");
dash.SetDecryptionKey(keyId, key);
WriteKeyFile($"KeyId={Convert.ToHexString(keyId)}{Environment.NewLine}Key={Convert.ToHexString(key)}");
//Remove meta box containing DRM info
if (DownloadOptions.FixupFile && dash.Moov.GetChild<Mpeg4Lib.Boxes.MetaBox>() is { } meta)
dash.Moov.Children.Remove(meta);
return dash;
}
protected bool Step_GetMetadata()
else if (DownloadOptions.InputType is FileType.Aax)
{
AaxFile = new AaxFile(InputFileStream);
if (DownloadOptions.StripUnabridged)
{
AaxFile.AppleTags.Title = AaxFile.AppleTags.TitleSansUnabridged;
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
}
//Finishing configuring lame encoder.
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
{
double bitrateMultiple = 1;
if (AaxFile.AudioChannels == 2)
{
if (DownloadOptions.Downsample)
bitrateMultiple = 0.5;
else
DownloadOptions.LameConfig.Mode = NAudio.Lame.MPEGMode.Stereo;
}
if (DownloadOptions.MatchSourceBitrate)
{
int kbps = (int)(AaxFile.AverageBitrate * bitrateMultiple / 1024);
if (DownloadOptions.LameConfig.VBR is null)
DownloadOptions.LameConfig.BitRate = kbps;
else if (DownloadOptions.LameConfig.VBR == NAudio.Lame.VBRMode.ABR)
DownloadOptions.LameConfig.ABRRateKbps = kbps;
}
}
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]");
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
RetrievedMetadata?.Invoke(this, AaxFile.AppleTags);
return !IsCanceled;
var aax = new AaxFile(InputFileStream);
var key = keys[0].KeyPart1;
aax.SetDecryptionKey(keys[0].KeyPart1);
WriteKeyFile($"ActivationBytes={Convert.ToHexString(key)}");
return aax;
}
protected DownloadProgress Step_DownloadAudiobook_Start()
else if (DownloadOptions.InputType is FileType.Aaxc)
{
var zeroProgress = new DownloadProgress
{
BytesReceived = 0,
ProgressPercentage = 0,
TotalBytesToReceive = InputFileStream.Length
};
OnDecryptProgressUpdate(zeroProgress);
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
return zeroProgress;
var aax = new AaxFile(InputFileStream);
var key = keys[0].KeyPart1;
var iv = keys[0].KeyPart2 ?? throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} for '{DownloadOptions.InputType}' must have a non-null initialization vector (KeyPart2).");
aax.SetDecryptionKey(keys[0].KeyPart1, iv);
WriteKeyFile($"Key={Convert.ToHexString(key)}{Environment.NewLine}IV={Convert.ToHexString(iv)}");
return aax;
}
else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown.");
protected void Step_DownloadAudiobook_End(DownloadProgress zeroProgress)
void WriteKeyFile(string contents)
{
AaxFile.Close();
CloseInputFileStream();
OnDecryptProgressUpdate(zeroProgress);
}
protected void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{
var duration = AaxFile.Duration;
var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
var progressPercent = (e.ProcessPosition / e.TotalDuration);
OnDecryptProgressUpdate(
new DownloadProgress
{
ProgressPercentage = 100 * progressPercent,
BytesReceived = (long)(InputFileStream.Length * progressPercent),
TotalBytesToReceive = InputFileStream.Length
});
}
public override void Cancel()
{
IsCanceled = true;
AaxFile?.Cancel();
AaxFile?.Dispose();
CloseInputFileStream();
var keyFile = Path.Combine(Path.ChangeExtension(InputFileStream.SaveFilePath, ".key"));
File.WriteAllText(keyFile, contents + Environment.NewLine);
OnTempFileCreated(new(keyFile));
}
}
protected bool Step_GetMetadata()
{
AaxFile = Open();
RetrievedMetadata?.Invoke(this, AaxFile.MetadataItems);
if (DownloadOptions.StripUnabridged)
{
AaxFile.MetadataItems.Title = AaxFile.MetadataItems.TitleSansUnabridged;
AaxFile.MetadataItems.Album = AaxFile.MetadataItems.Album?.Replace(" (Unabridged)", "");
}
if (DownloadOptions.FixupFile)
{
if (!string.IsNullOrWhiteSpace(AaxFile.MetadataItems.Narrator))
AaxFile.MetadataItems.AppleListBox.EditOrAddTag("©wrt", AaxFile.MetadataItems.Narrator);
if (!string.IsNullOrWhiteSpace(AaxFile.MetadataItems.Copyright))
AaxFile.MetadataItems.Copyright = AaxFile.MetadataItems.Copyright.Replace("(P)", "℗").Replace("&#169;", "©");
//Add audiobook shelf tags
//https://github.com/advplyr/audiobookshelf/issues/1794#issuecomment-1565050213
const string tagDomain = "com.pilabor.tone";
AaxFile.MetadataItems.Title = DownloadOptions.Title;
if (DownloadOptions.Subtitle is string subtitle)
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "SUBTITLE", subtitle);
if (DownloadOptions.Publisher is string publisher)
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "PUBLISHER", publisher);
if (DownloadOptions.Language is string language)
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "LANGUAGE", language);
if (DownloadOptions.AudibleProductId is string asin)
{
AaxFile.MetadataItems.Asin = asin;
AaxFile.MetadataItems.AppleListBox.EditOrAddTag("asin", asin);
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ASIN", asin);
}
if (DownloadOptions.SeriesName is string series)
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
if (DownloadOptions.SeriesNumber is string part)
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part);
}
OnRetrievedTitle(AaxFile.MetadataItems.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.MetadataItems.FirstAuthor);
OnRetrievedNarrators(AaxFile.MetadataItems.Narrator);
OnRetrievedCoverArt(AaxFile.MetadataItems.Cover);
OnInitialized();
return !IsCanceled;
}
protected virtual void OnInitialized() { }
}
+100 -122
View File
@@ -1,141 +1,119 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AAXClean;
using AAXClean;
using AAXClean.Codecs;
using Dinah.Core.StepRunner;
using FileManager;
using Mpeg4Lib;
using System;
using System.IO;
using System.Threading.Tasks;
namespace AaxDecrypter
namespace AaxDecrypter;
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
{
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3);
private FileStream? workingFileStream;
public AaxcDownloadMultiConverter(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
: base(outDirectory, cacheDirectory, dlOptions)
{
protected override StepSequence Steps { get; }
AsyncSteps.Name = $"Download, Convert Aaxc To {DownloadOptions.OutputFormat}, and Split";
AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
}
private Func<MultiConvertFileProperties, string> multipartFileNameCallback { get; }
protected override void OnInitialized()
{
//Finishing configuring lame encoder.
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
{
if (AaxFile is null)
throw new InvalidOperationException($"AaxFile is null during {nameof(OnInitialized)} in {nameof(AaxcDownloadConvertBase)}.");
if (DownloadOptions.LameConfig is null)
throw new InvalidOperationException($"LameConfig is null during {nameof(OnInitialized)} in {nameof(DownloadOptions)}.");
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private List<string> multiPartFilePaths { get; } = new List<string>();
MpegUtil.ConfigureLameOptions(
AaxFile,
DownloadOptions.LameConfig,
DownloadOptions.Downsample,
DownloadOptions.MatchSourceBitrate,
chapters: null);
}
}
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic,
Func<MultiConvertFileProperties, string> multipartFileNameCallback = null)
: base(outFileName, cacheDirectory, dlLic)
{
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat,
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
["Step 3: Cleanup"] = Step_Cleanup,
};
this.multipartFileNameCallback = multipartFileNameCallback ?? MultiConvertFileProperties.DefaultMultipartFilename;
}
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{
if (AaxFile is null) return false;
/*
https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489
try
{
await (AaxConversion = decryptMultiAsync(AaxFile, DownloadOptions.ChapterInfo));
If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored.
If the chapter is shorter than 3 seconds long but still has some audio frames, those frames are combined with the following chapter and not split into a new file.
if (AaxConversion.IsCompletedSuccessfully)
await moveMoovToBeginning(AaxFile, workingFileStream?.Name);
I also implemented file naming by chapter title. When 2 or more consecutive chapters are combined, the first of the combined chapter's title is used in the file name. For example, given an audiobook with the following chapters:
return AaxConversion.IsCompletedSuccessfully;
}
finally
{
workingFileStream?.Dispose();
FinalizeDownload();
}
}
00:00:00 - 00:00:02 | Part 1
00:00:02 - 00:35:00 | Chapter 1
00:35:02 - 01:02:00 | Chapter 2
01:02:00 - 01:02:02 | Part 2
01:02:02 - 01:41:00 | Chapter 3
01:41:00 - 02:05:00 | Chapter 4
private Mp4Operation decryptMultiAsync(Mp4File aaxFile, ChapterInfo splitChapters)
{
var chapterCount = 0;
return
DownloadOptions.OutputFormat == OutputFormat.M4b
? aaxFile.ConvertToMultiMp4aAsync
(
splitChapters,
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback)
)
: aaxFile.ConvertToMultiMp3Async
(
splitChapters,
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
DownloadOptions.LameConfig
);
The book will be split into the following files:
void newSplit(int currentChapter, ChapterInfo splitChapters, INewSplitCallback newSplitCallback)
{
moveMoovToBeginning(aaxFile, workingFileStream?.Name).GetAwaiter().GetResult();
var newTempFile = GetNewTempFilePath(DownloadOptions.OutputFormat.ToString());
MultiConvertFileProperties props = new()
{
OutputFileName = newTempFile.FilePath,
PartsPosition = currentChapter,
PartsTotal = splitChapters.Count,
Title = newSplitCallback.Chapter?.Title,
};
00:00:00 - 00:35:00 | Book - 01 - Part 1.m4b
00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b
01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b
01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b
newSplitCallback.OutputFile = workingFileStream = createOutputFileStream(props);
newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitle(props);
newSplitCallback.TrackNumber = currentChapter;
newSplitCallback.TrackCount = splitChapters.Count;
That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name.
*/
private bool Step_DownloadAudiobookAsMultipleFilesPerChapter()
{
var zeroProgress = Step_DownloadAudiobook_Start();
OnTempFileCreated(newTempFile with { PartProperties = props });
}
var chapters = DownloadOptions.ChapterInfo.Chapters.ToList();
FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
{
FileUtility.SaferDelete(multiConvertFileProperties.OutputFileName);
return File.Open(multiConvertFileProperties.OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
}
}
// Ensure split files are at least minChapterLength in duration.
var splitChapters = new ChapterInfo(DownloadOptions.ChapterInfo.StartOffset);
var runningTotal = TimeSpan.Zero;
string title = "";
for (int i = 0; i < chapters.Count; i++)
{
if (runningTotal == TimeSpan.Zero)
title = chapters[i].Title;
runningTotal += chapters[i].Duration;
if (runningTotal >= minChapterLength)
{
splitChapters.AddChapter(title, runningTotal);
runningTotal = TimeSpan.Zero;
}
}
// reset, just in case
multiPartFilePaths.Clear();
ConversionResult result;
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
if (DownloadOptions.OutputFormat == OutputFormat.M4b)
result = ConvertToMultiMp4a(splitChapters);
else
result = ConvertToMultiMp3(splitChapters);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step_DownloadAudiobook_End(zeroProgress);
return result == ConversionResult.NoErrorsDetected;
}
private ConversionResult ConvertToMultiMp4a(ChapterInfo splitChapters)
{
var chapterCount = 0;
return AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback),
DownloadOptions.TrimOutputToChapterLength);
}
private ConversionResult ConvertToMultiMp3(ChapterInfo splitChapters)
{
var chapterCount = 0;
return AaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
{
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback);
((NAudio.Lame.LameConfig)newSplitCallback.UserState).ID3.Track = chapterCount.ToString();
}, DownloadOptions.LameConfig, DownloadOptions.TrimOutputToChapterLength);
}
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
{
var fileName = multipartFileNameCallback(new()
{
OutputFileName = OutputFileName,
PartsPosition = currentChapter,
PartsTotal = splitChapters.Count,
Title = newSplitCallback?.Chapter?.Title
});
fileName = FileUtility.GetValidFilename(fileName);
multiPartFilePaths.Add(fileName);
FileUtility.SaferDelete(fileName);
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
OnFileCreated(fileName);
}
}
private Mp4Operation moveMoovToBeginning(Mp4File aaxFile, string? filename)
{
if (DownloadOptions.OutputFormat is OutputFormat.M4b
&& DownloadOptions.MoveMoovToBeginning
&& filename is not null
&& File.Exists(filename))
{
return Mp4File.RelocateMoovAsync(filename);
}
else return Mp4Operation.FromCompleted(aaxFile);
}
}
@@ -1,55 +1,113 @@
using System;
using System.IO;
using AAXClean;
using AAXClean;
using AAXClean.Codecs;
using Dinah.Core.StepRunner;
using Dinah.Core.Net.Http;
using FileManager;
using System;
using System.IO;
using System.Threading.Tasks;
namespace AaxDecrypter
namespace AaxDecrypter;
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
{
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
private readonly AverageSpeed averageSpeed = new();
private TempFile? outputTempFile;
public AaxcDownloadSingleConverter(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
: base(outDirectory, cacheDirectory, dlOptions)
{
protected override StepSequence Steps { get; }
var step = 1;
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic)
{
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat,
AsyncSteps.Name = $"Download and Convert Aaxc To {DownloadOptions.OutputFormat}";
AsyncSteps[$"Step {step++}: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
AsyncSteps[$"Step {step++}: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
if (DownloadOptions.MoveMoovToBeginning && DownloadOptions.OutputFormat is OutputFormat.M4b)
AsyncSteps[$"Step {step++}: Move moov atom to beginning"] = Step_MoveMoov;
AsyncSteps[$"Step {step++}: Create Cue"] = Step_CreateCueAsync;
}
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsSingleFile,
["Step 3: Create Cue"] = Step_CreateCue,
["Step 4: Cleanup"] = Step_Cleanup,
};
}
protected override void OnInitialized()
{
//Finishing configuring lame encoder.
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
{
if (AaxFile is null)
throw new InvalidOperationException($"AaxFile is null during {nameof(OnInitialized)} in {nameof(AaxcDownloadConvertBase)}.");
if (DownloadOptions.LameConfig is null)
throw new InvalidOperationException($"LameConfig is null during {nameof(OnInitialized)} in {nameof(DownloadOptions)}.");
private bool Step_DownloadAudiobookAsSingleFile()
{
var zeroProgress = Step_DownloadAudiobook_Start();
MpegUtil.ConfigureLameOptions(
AaxFile,
DownloadOptions.LameConfig,
DownloadOptions.Downsample,
DownloadOptions.MatchSourceBitrate,
DownloadOptions.ChapterInfo);
}
}
FileUtility.SaferDelete(OutputFileName);
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{
if (AaxFile is null) return false;
outputTempFile = GetNewTempFilePath(DownloadOptions.OutputFormat.ToString());
FileUtility.SaferDelete(outputTempFile.FilePath);
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OnFileCreated(OutputFileName);
using var outputFile = File.Open(outputTempFile.FilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OnTempFileCreated(outputTempFile);
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
var decryptionResult
= DownloadOptions.OutputFormat == OutputFormat.M4b
? AaxFile.ConvertToMp4a(outputFile, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength)
: AaxFile.ConvertToMp3(outputFile, DownloadOptions.LameConfig, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
try
{
await (AaxConversion = decryptAsync(AaxFile, outputFile));
DownloadOptions.ChapterInfo = AaxFile.Chapters;
return AaxConversion.IsCompletedSuccessfully;
}
finally
{
FinalizeDownload();
}
}
Step_DownloadAudiobook_End(zeroProgress);
private async Task<bool> Step_MoveMoov()
{
if (outputTempFile is null) return false;
AaxConversion = Mp4File.RelocateMoovAsync(outputTempFile.FilePath);
AaxConversion.ConversionProgressUpdate += AaxConversion_MoovProgressUpdate;
await AaxConversion;
AaxConversion.ConversionProgressUpdate -= AaxConversion_MoovProgressUpdate;
return AaxConversion.IsCompletedSuccessfully;
}
var success = decryptionResult == ConversionResult.NoErrorsDetected && !IsCanceled;
if (success)
base.OnFileCreated(OutputFileName);
private void AaxConversion_MoovProgressUpdate(object? sender, ConversionProgressEventArgs e)
{
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
return success;
}
}
var remainingTimeToProcess = (e.EndTime - e.ProcessPosition).TotalSeconds;
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average;
if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
OnDecryptProgressUpdate(
new DownloadProgress
{
ProgressPercentage = 100 * e.FractionCompleted,
BytesReceived = (long)(InputFileStream.Length * e.FractionCompleted),
TotalBytesToReceive = InputFileStream.Length
});
}
private Mp4Operation decryptAsync(Mp4File aaxFile, Stream outputFile)
=> DownloadOptions.OutputFormat == OutputFormat.Mp3
? aaxFile.ConvertToMp3Async
(
outputFile,
DownloadOptions.LameConfig,
DownloadOptions.ChapterInfo
)
: DownloadOptions.FixupFile
? aaxFile.ConvertToMp4aAsync
(
outputFile,
DownloadOptions.ChapterInfo
)
: aaxFile.ConvertToMp4aAsync(outputFile);
}
+191 -144
View File
@@ -1,178 +1,225 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dinah.Core;
using Dinah.Core;
using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner;
using FileManager;
using System;
using System.IO;
using System.Threading.Tasks;
namespace AaxDecrypter
namespace AaxDecrypter;
public enum OutputFormat { M4b, Mp3 }
public abstract class AudiobookDownloadBase
{
public enum OutputFormat { M4b, Mp3 }
public event EventHandler<string?>? RetrievedTitle;
public event EventHandler<string?>? RetrievedAuthors;
public event EventHandler<string?>? RetrievedNarrators;
public event EventHandler<byte[]?>? RetrievedCoverArt;
public event EventHandler<DownloadProgress>? DecryptProgressUpdate;
public event EventHandler<TimeSpan>? DecryptTimeRemaining;
public event EventHandler<TempFile>? TempFileCreated;
public abstract class AudiobookDownloadBase
public bool IsCanceled { get; protected set; }
protected AsyncStepSequence AsyncSteps { get; } = new();
protected string OutputDirectory { get; }
public IDownloadOptions DownloadOptions { get; }
protected NetworkFileStream InputFileStream => NfsPersister.NetworkFileStream;
protected virtual long InputFilePosition
{
public event EventHandler<string> RetrievedTitle;
public event EventHandler<string> RetrievedAuthors;
public event EventHandler<string> RetrievedNarrators;
public event EventHandler<byte[]> RetrievedCoverArt;
public event EventHandler<DownloadProgress> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining;
public event EventHandler<string> FileCreated;
public bool IsCanceled { get; set; }
protected string OutputFileName { get; private set; }
protected DownloadOptions DownloadOptions { get; }
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
// Don't give the property a 'set'. This should have to be an obvious choice; not accidental
protected void SetOutputFileName(string newOutputFileName) => OutputFileName = newOutputFileName;
protected abstract StepSequence Steps { get; }
private NetworkFileStreamPersister nfsPersister;
private string jsonDownloadState { get; }
public string TempFilePath { get; }
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
get
{
OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
var outDir = Path.GetDirectoryName(OutputFileName);
if (!Directory.Exists(outDir))
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(outDir)}");
if (!Directory.Exists(cacheDirectory))
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}");
jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json")));
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
DownloadOptions = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
// delete file after validation is complete
FileUtility.SaferDelete(OutputFileName);
//Use try/catch instread of checking CanRead to avoid
//a race with the background download completing
//between the check and the Position call.
try { return InputFileStream.Position; }
catch { return InputFileStream.Length; }
}
}
private bool downloadFinished;
public abstract void Cancel();
private NetworkFileStreamPersister? m_nfsPersister;
private NetworkFileStreamPersister NfsPersister => m_nfsPersister ??= OpenNetworkFileStream();
private readonly DownloadProgress zeroProgress;
private readonly string jsonDownloadState;
private readonly string tempFilePath;
public virtual void SetCoverArt(byte[] coverArt)
protected AudiobookDownloadBase(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
{
OutputDirectory = ArgumentValidator.EnsureNotNullOrWhiteSpace(outDirectory, nameof(outDirectory));
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed;
if (!Directory.Exists(OutputDirectory))
Directory.CreateDirectory(OutputDirectory);
if (!Directory.Exists(cacheDirectory))
Directory.CreateDirectory(cacheDirectory);
jsonDownloadState = Path.Combine(cacheDirectory, $"{DownloadOptions.AudibleProductId}.json");
tempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
zeroProgress = new DownloadProgress
{
if (coverArt is not null)
OnRetrievedCoverArt(coverArt);
}
BytesReceived = 0,
ProgressPercentage = 0,
TotalBytesToReceive = 0
};
public bool Run()
OnDecryptProgressUpdate(zeroProgress);
}
protected TempFile GetNewTempFilePath(string extension)
{
extension = FileUtility.GetStandardizedExtension(extension);
var path = Path.Combine(OutputDirectory, Guid.NewGuid().ToString("N") + extension);
return new(path, extension);
}
public async Task<bool> RunAsync()
{
await InputFileStream.BeginDownloadingAsync();
var progressTask = Task.Run(reportProgress);
(bool success, var elapsed) = await AsyncSteps.RunAsync();
//Stop the downloader so it doesn't keep running in the background.
if (!success)
NfsPersister.Dispose();
await progressTask;
var speedup = DownloadOptions.RuntimeLength / elapsed;
Serilog.Log.Information($"Speedup is {speedup:F0}x realtime.");
NfsPersister.Dispose();
return success;
async Task reportProgress()
{
var (IsSuccess, Elapsed) = Steps.Run();
AverageSpeed averageSpeed = new();
if (!IsSuccess)
Serilog.Log.Logger.Error("Conversion failed");
return IsSuccess;
}
protected void OnRetrievedTitle(string title)
=> RetrievedTitle?.Invoke(this, title);
protected void OnRetrievedAuthors(string authors)
=> RetrievedAuthors?.Invoke(this, authors);
protected void OnRetrievedNarrators(string narrators)
=> RetrievedNarrators?.Invoke(this, narrators);
protected void OnRetrievedCoverArt(byte[] coverArt)
=> RetrievedCoverArt?.Invoke(this, coverArt);
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
=> DecryptTimeRemaining?.Invoke(this, timeRemaining);
protected void OnFileCreated(string path)
=> FileCreated?.Invoke(this, path);
protected void CloseInputFileStream()
{
nfsPersister?.NetworkFileStream?.Close();
nfsPersister?.Dispose();
}
protected bool Step_CreateCue()
{
if (!DownloadOptions.CreateCueSheet) return true;
// not a critical step. its failure should not prevent future steps from running
try
while (
InputFileStream.CanRead
&& InputFileStream.Length > InputFilePosition
&& !InputFileStream.IsCancelled
&& !downloadFinished)
{
var path = Path.ChangeExtension(OutputFileName, ".cue");
path = FileUtility.GetValidFilename(path);
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
OnFileCreated(path);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Step_CreateCue)}. FAILED");
averageSpeed.AddPosition(InputFilePosition);
var estSecsRemaining = (InputFileStream.Length - InputFilePosition) / averageSpeed.Average;
if (double.IsNormal(estSecsRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estSecsRemaining));
var progressPercent = 100d * InputFilePosition / InputFileStream.Length;
OnDecryptProgressUpdate(
new DownloadProgress
{
ProgressPercentage = progressPercent,
BytesReceived = InputFilePosition,
TotalBytesToReceive = InputFileStream.Length
});
await Task.Delay(200);
}
OnDecryptTimeRemaining(TimeSpan.Zero);
OnDecryptProgressUpdate(zeroProgress);
}
}
public virtual Task CancelAsync()
{
IsCanceled = true;
FinalizeDownload();
return Task.CompletedTask;
}
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
public virtual void SetCoverArt(byte[] coverArt) { }
protected void OnRetrievedTitle(string? title)
=> RetrievedTitle?.Invoke(this, title);
protected void OnRetrievedAuthors(string? authors)
=> RetrievedAuthors?.Invoke(this, authors);
protected void OnRetrievedNarrators(string? narrators)
=> RetrievedNarrators?.Invoke(this, narrators);
protected void OnRetrievedCoverArt(byte[]? coverArt)
=> RetrievedCoverArt?.Invoke(this, coverArt);
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
=> DecryptTimeRemaining?.Invoke(this, timeRemaining);
public void OnTempFileCreated(TempFile path)
=> TempFileCreated?.Invoke(this, path);
protected virtual void FinalizeDownload()
{
NfsPersister.Dispose();
downloadFinished = true;
}
protected async Task<bool> Step_CreateCueAsync()
{
if (!DownloadOptions.CreateCueSheet) return !IsCanceled;
if (DownloadOptions.ChapterInfo.Count <= 1)
{
Serilog.Log.Logger.Information($"Skipped creating .cue because book has no chapters.");
return !IsCanceled;
}
protected bool Step_Cleanup()
// not a critical step. its failure should not prevent future steps from running
try
{
bool success = !IsCanceled;
if (success)
{
FileUtility.SaferDelete(jsonDownloadState);
if (DownloadOptions.AudibleKey is not null &&
DownloadOptions.AudibleIV is not null &&
DownloadOptions.RetainEncryptedFile)
{
string aaxPath = Path.ChangeExtension(TempFilePath, ".aax");
FileUtility.SaferMove(TempFilePath, aaxPath);
//Write aax decryption key
string keyPath = Path.ChangeExtension(aaxPath, ".key");
FileUtility.SaferDelete(keyPath);
File.WriteAllText(keyPath, $"Key={DownloadOptions.AudibleKey}\r\nIV={DownloadOptions.AudibleIV}");
OnFileCreated(aaxPath);
OnFileCreated(keyPath);
}
else
FileUtility.SaferDelete(TempFilePath);
}
return success;
var tempFile = GetNewTempFilePath(".cue");
await File.WriteAllTextAsync(tempFile.FilePath, Cue.CreateContents(Path.GetFileName(tempFile.FilePath), DownloadOptions.ChapterInfo));
OnTempFileCreated(tempFile);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Step_CreateCueAsync)} Failed");
}
return !IsCanceled;
}
private NetworkFileStreamPersister OpenNetworkFileStream()
private NetworkFileStreamPersister OpenNetworkFileStream()
{
NetworkFileStreamPersister? nfsp = default;
try
{
if (!File.Exists(jsonDownloadState))
return NewNetworkFilePersister();
return nfsp = newNetworkFilePersister();
try
nfsp = new NetworkFileStreamPersister(jsonDownloadState);
// The download url expires after 1 hour.
// The new url points to the same file.
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl));
return nfsp;
}
catch
{
nfsp?.Target?.Dispose();
FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(tempFilePath);
return nfsp = newNetworkFilePersister();
}
finally
{
//nfsp will only be null when an unhandled exception occurs. Let the caller handle it.
if (nfsp is not null)
{
var nfsp = new NetworkFileStreamPersister(jsonDownloadState);
// If More than ~1 hour has elapsed since getting the download url, it will expire.
// The new url will be to the same file.
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl));
return nfsp;
}
catch
{
FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(TempFilePath);
return NewNetworkFilePersister();
nfsp.NetworkFileStream.RequestHeaders["User-Agent"] = DownloadOptions.UserAgent;
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
OnTempFileCreated(new(tempFilePath, DownloadOptions.InputType.ToString()));
OnTempFileCreated(new(jsonDownloadState));
}
}
private NetworkFileStreamPersister NewNetworkFilePersister()
NetworkFileStreamPersister newNetworkFilePersister()
{
var headers = new System.Net.WebHeaderCollection
{
{ "User-Agent", DownloadOptions.UserAgent }
};
var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, headers);
var networkFileStream = new NetworkFileStream(tempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } });
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
}
+171
View File
@@ -0,0 +1,171 @@
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.Linq;
namespace AaxDecrypter;
public static class LinqStats
{
public static (double mean, double stdDev) BasicStatisticsBy<T>(this IEnumerable<T> values, Func<T, double> selector)
{
var count = values.Count();
var mean = values.Average(selector);
return (mean, Math.Sqrt(values.Sum(s => Math.Pow(selector(s) - mean, 2)) / (count - 1)));
}
public static bool T_Test_2By<T>(this IEnumerable<T> values, Func<T, double> selector, IEnumerable<T> secondGroup, Significance confidence)
{
var n1 = values.Count();
var n2 = secondGroup.Count();
var n = n1 + n2;
if (n1 < 3 || n2 < 3) return false;
(var mean1, var stdDev1) = values.BasicStatisticsBy(selector);
(var mean2, var stdDev2) = secondGroup.BasicStatisticsBy(selector);
var pooledStdDev = Math.Sqrt((((n1 - 1) * (stdDev1 * stdDev1)) + ((n2 - 1) * (stdDev2 * stdDev2))) / (n1 + n2 - 2));
var testStat = Math.Abs(mean1 - mean2) / (pooledStdDev * Math.Sqrt(1d / n1 + 1d / n2));
var crit = T_Stat(Math.Min(n - 2, MAX_DEGREES_FREEDOM), confidence);
return testStat > crit;
}
public static bool T_Test_1By<T>(this IEnumerable<T> values, Func<T, double> selector, double testMean, Significance confidence)
{
var n = values.Count();
if (n < 2) return false;
(var sampleMean, var sampleStdDev) = values.BasicStatisticsBy(selector);
var testStat = Math.Abs(sampleMean - testMean) / (sampleStdDev / Math.Sqrt(n));
var crit = T_Stat(Math.Min(n - 1, MAX_DEGREES_FREEDOM), confidence);
return testStat > crit;
}
private static double T_Stat(int degreesFreedom, Significance confidence)
{
ArgumentValidator.EnsureBetweenInclusive(degreesFreedom, nameof(degreesFreedom), MIN_DEGREES_FREEDOM, MAX_DEGREES_FREEDOM);
return T_TABLE[(int)confidence][degreesFreedom - MIN_DEGREES_FREEDOM];
}
static LinqStats()
{
T_TABLE = new double[][] { T_Table_01, T_Table_05, T_Table_10, T_Table_15, T_Table_20, T_Table_25 };
}
private const int MIN_DEGREES_FREEDOM = 1;
private const int MAX_DEGREES_FREEDOM = 201;
/// <summary>
/// 2-tailed t-Distribution critical values at 75%, 80%, 85%,
/// 90%, 95%, and 99% confidence for 1 - 201 degrees of freedom.
/// </summary>
private static readonly double[][] T_TABLE;
private static readonly double[] T_Table_25 = { 2.414213562, 1.603567451, 1.422625281, 1.344397556, 1.300949037, 1.273349309, 1.254278682, 1.240318261, 1.229659173, 1.221255395, 1.214460246, 1.208852542, 1.204146242, 1.200140298, 1.196689284, 1.193685414, 1.191047107, 1.188711483, 1.186629298, 1.184761434, 1.183076432, 1.181548697, 1.180157199, 1.178884497, 1.177716003, 1.176639425, 1.175644329, 1.174721803, 1.173864189, 1.173064871, 1.1723181, 1.17161886, 1.170962753, 1.17034591, 1.169764906, 1.169216709, 1.168698615, 1.168208212, 1.167743338, 1.167302049, 1.166882595, 1.166483396, 1.166103019, 1.165740162, 1.165393644, 1.165062385, 1.164745398, 1.164441782, 1.164150707, 1.163871412, 1.163603196, 1.163345413, 1.163097467, 1.162858803, 1.162628911, 1.162407316, 1.162193577, 1.161987283, 1.161788052, 1.161595527, 1.161409375, 1.161229286, 1.161054967, 1.160886145, 1.160722566, 1.160563987, 1.160410184, 1.160260944, 1.160116066, 1.159975363, 1.159838656, 1.159705777, 1.159576569, 1.15945088, 1.15932857, 1.159209503, 1.159093552, 1.158980598, 1.158870524, 1.158763222, 1.158658589, 1.158556526, 1.15845694, 1.158359742, 1.158264847, 1.158172173, 1.158081645, 1.157993188, 1.157906731, 1.157822209, 1.157739556, 1.157658712, 1.157579617, 1.157502216, 1.157426454, 1.157352281, 1.157279646, 1.157208502, 1.157138804, 1.157070509, 1.157003573, 1.156937958, 1.156873624, 1.156810534, 1.156748653, 1.156687945, 1.156628379, 1.156569922, 1.156512543, 1.156456213, 1.156400904, 1.156346587, 1.156293237, 1.156240827, 1.156189334, 1.156138733, 1.156089001, 1.156040117, 1.155992058, 1.155944804, 1.155898335, 1.155852631, 1.155807674, 1.155763446, 1.155719928, 1.155677105, 1.155634959, 1.155593475, 1.155552637, 1.15551243, 1.155472839, 1.155433851, 1.155395452, 1.155357629, 1.155320368, 1.155283658, 1.155247486, 1.155211841, 1.15517671, 1.155142084, 1.15510795, 1.1550743, 1.155041122, 1.155008406, 1.154976144, 1.154944326, 1.154912942, 1.154881984, 1.154851443, 1.154821311, 1.15479158, 1.154762241, 1.154733287, 1.154704711, 1.154676505, 1.154648662, 1.154621175, 1.154594037, 1.154567242, 1.154540783, 1.154514654, 1.154488849, 1.154463361, 1.154438185, 1.154413316, 1.154388747, 1.154364474, 1.15434049, 1.154316792, 1.154293373, 1.154270229, 1.154247355, 1.154224746, 1.154202398, 1.154180307, 1.154158467, 1.154136875, 1.154115526, 1.154094417, 1.154073543, 1.1540529, 1.154032485, 1.154012294, 1.153992323, 1.153972568, 1.153953027, 1.153933695, 1.15391457, 1.153895647, 1.153876925, 1.153858399, 1.153840066, 1.153821925, 1.15380397, 1.153786201, 1.153768613, 1.153751204, 1.153733972, 1.153716914, 1.153700026 };
private static readonly double[] T_Table_20 = { 3.077683537, 1.885618083, 1.637744354, 1.533206274, 1.475884049, 1.439755747, 1.414923928, 1.39681531, 1.383028738, 1.372183641, 1.363430318, 1.356217334, 1.350171289, 1.345030374, 1.340605608, 1.336757167, 1.33337939, 1.330390944, 1.327728209, 1.325340707, 1.323187874, 1.321236742, 1.31946024, 1.317835934, 1.316345073, 1.314971864, 1.313702913, 1.312526782, 1.311433647, 1.310415025, 1.309463549, 1.308572793, 1.307737124, 1.306951587, 1.306211802, 1.305513886, 1.304854381, 1.304230204, 1.303638589, 1.303077053, 1.302543359, 1.302035487, 1.301551608, 1.30109006, 1.300649332, 1.300228048, 1.299824947, 1.299438879, 1.299068785, 1.298713694, 1.298372713, 1.298045016, 1.297729843, 1.297426488, 1.2971343, 1.296852673, 1.296581044, 1.29631889, 1.296065725, 1.295821094, 1.295584571, 1.295355762, 1.295134294, 1.29491982, 1.294712013, 1.294510568, 1.294315197, 1.294125629, 1.293941609, 1.293762898, 1.293589269, 1.293420507, 1.293256413, 1.293096793, 1.292941469, 1.292790268, 1.292643029, 1.292499597, 1.292359828, 1.292223583, 1.29209073, 1.291961144, 1.291834705, 1.291711301, 1.291590824, 1.291473171, 1.291358243, 1.291245948, 1.291136195, 1.291028899, 1.290923979, 1.290821356, 1.290720956, 1.290622708, 1.290526543, 1.290432395, 1.290340202, 1.290249904, 1.290161442, 1.290074761, 1.289989809, 1.289906533, 1.289824884, 1.289744816, 1.289666283, 1.289589241, 1.289513648, 1.289439464, 1.289366649, 1.289295166, 1.289224979, 1.289156054, 1.289088355, 1.289021851, 1.28895651, 1.288892302, 1.288829199, 1.288767171, 1.288706191, 1.288646234, 1.288587273, 1.288529284, 1.288472243, 1.288416127, 1.288360913, 1.288306581, 1.288253109, 1.288200477, 1.288148665, 1.288097654, 1.288047427, 1.287997964, 1.287949248, 1.287901264, 1.287853994, 1.287807422, 1.287761534, 1.287716314, 1.287671748, 1.287627821, 1.287584521, 1.287541833, 1.287499745, 1.287458245, 1.287417319, 1.287376957, 1.287337146, 1.287297876, 1.287259135, 1.287220914, 1.2871832, 1.287145985, 1.287109259, 1.287073012, 1.287037235, 1.287001918, 1.286967053, 1.286932631, 1.286898644, 1.286865084, 1.286831942, 1.286799212, 1.286766884, 1.286734952, 1.286703409, 1.286672248, 1.286641461, 1.286611042, 1.286580985, 1.286551283, 1.286521929, 1.286492918, 1.286464244, 1.286435901, 1.286407882, 1.286380184, 1.286352799, 1.286325724, 1.286298952, 1.286272479, 1.286246299, 1.286220408, 1.286194801, 1.286169474, 1.286144421, 1.286119638, 1.286095122, 1.286070867, 1.28604687, 1.286023127, 1.285999633, 1.285976384, 1.285953377, 1.285930609, 1.285908074, 1.285885771, 1.285863694, 1.285841842, 1.285820209, 1.285798794 };
private static readonly double[] T_Table_15 = { 4.16529977, 2.281930588, 1.924319657, 1.778192164, 1.699362566, 1.650173154, 1.616591737, 1.59222144, 1.573735785, 1.559235933, 1.547559766, 1.537956495, 1.529919606, 1.523095061, 1.517227969, 1.51213017, 1.507659754, 1.503707672, 1.500188756, 1.497035518, 1.494193795, 1.491619612, 1.489276897, 1.487135783, 1.485171326, 1.483362535, 1.481691617, 1.48014339, 1.478704821, 1.477364662, 1.47611315, 1.474941772, 1.473843072, 1.47281049, 1.471838233, 1.470921166, 1.470054719, 1.469234815, 1.468457801, 1.467720399, 1.467019655, 1.466352901, 1.465717725, 1.465111933, 1.464533534, 1.463980712, 1.463451805, 1.462945295, 1.46245979, 1.461994009, 1.461546775, 1.461117, 1.460703683, 1.460305896, 1.45992278, 1.459553538, 1.45919743, 1.458853767, 1.458521908, 1.458201256, 1.457891251, 1.457591373, 1.457301133, 1.457020074, 1.456747768, 1.45648381, 1.456227824, 1.455979454, 1.455738365, 1.455504241, 1.455276784, 1.455055715, 1.454840767, 1.45463169, 1.454428246, 1.454230212, 1.454037373, 1.453849529, 1.453666487, 1.453488066, 1.453314093, 1.453144404, 1.452978842, 1.452817259, 1.452659513, 1.452505469, 1.452354998, 1.452207977, 1.452064289, 1.451923821, 1.451786468, 1.451652126, 1.451520697, 1.451392088, 1.451266209, 1.451142973, 1.451022299, 1.450904108, 1.450788323, 1.450674871, 1.450563684, 1.450454694, 1.450347836, 1.450243048, 1.450140271, 1.450039448, 1.449940523, 1.449843444, 1.449748158, 1.449654617, 1.449562773, 1.449472581, 1.449383997, 1.449296977, 1.449211481, 1.449127468, 1.449044902, 1.448963744, 1.448883959, 1.448805513, 1.448728372, 1.448652503, 1.448577876, 1.44850446, 1.448432226, 1.448361146, 1.448291192, 1.448222337, 1.448154557, 1.448087826, 1.44802212, 1.447957415, 1.447893688, 1.447830919, 1.447769085, 1.447708165, 1.44764814, 1.44758899, 1.447530695, 1.447473238, 1.447416601, 1.447360765, 1.447305715, 1.447251433, 1.447197905, 1.447145113, 1.447093044, 1.447041682, 1.446991013, 1.446941023, 1.446891698, 1.446843026, 1.446794994, 1.446747588, 1.446700797, 1.446654609, 1.446609012, 1.446563996, 1.446519548, 1.446475659, 1.446432318, 1.446389514, 1.446347238, 1.44630548, 1.44626423, 1.44622348, 1.44618322, 1.446143442, 1.446104137, 1.446065296, 1.446026911, 1.445988975, 1.44595148, 1.445914417, 1.44587778, 1.445841561, 1.445805753, 1.445770349, 1.445735343, 1.445700727, 1.445666495, 1.445632641, 1.445599159, 1.445566042, 1.445533284, 1.445500881, 1.445468825, 1.445437112, 1.445405736, 1.445374691, 1.445343973, 1.445313576, 1.445283495, 1.445253726, 1.445224264, 1.445195103, 1.445166239, 1.445137668, 1.445109385, 1.445081387 };
private static readonly double[] T_Table_10 = { 6.313751515, 2.91998558, 2.353363435, 2.131846786, 2.015048373, 1.943180281, 1.894578605, 1.859548038, 1.833112933, 1.812461123, 1.795884819, 1.782287556, 1.770933396, 1.761310136, 1.753050356, 1.745883676, 1.739606726, 1.734063607, 1.729132812, 1.724718243, 1.720742903, 1.717144374, 1.713871528, 1.71088208, 1.708140761, 1.70561792, 1.703288446, 1.701130934, 1.699127027, 1.697260887, 1.695518783, 1.693888748, 1.692360309, 1.690924255, 1.689572458, 1.688297714, 1.68709362, 1.68595446, 1.684875122, 1.683851013, 1.682878002, 1.681952357, 1.681070703, 1.680229977, 1.679427393, 1.678660414, 1.677926722, 1.677224196, 1.676550893, 1.675905025, 1.67528495, 1.674689154, 1.674116237, 1.673564906, 1.673033965, 1.672522303, 1.672028888, 1.671552762, 1.671093032, 1.670648865, 1.670219484, 1.669804163, 1.669402222, 1.669013025, 1.668635976, 1.668270514, 1.667916114, 1.667572281, 1.667238549, 1.666914479, 1.666599658, 1.666293696, 1.665996224, 1.665706893, 1.665425373, 1.665151353, 1.664884537, 1.664624645, 1.664371409, 1.664124579, 1.663883913, 1.663649184, 1.663420175, 1.663196679, 1.6629785, 1.662765449, 1.662557349, 1.662354029, 1.662155326, 1.661961084, 1.661771155, 1.661585397, 1.661403674, 1.661225855, 1.661051817, 1.66088144, 1.66071461, 1.660551217, 1.660391156, 1.660234326, 1.66008063, 1.659929976, 1.659782273, 1.659637437, 1.659495383, 1.659356034, 1.659219312, 1.659085144, 1.658953458, 1.658824187, 1.658697265, 1.658572629, 1.658450216, 1.658329969, 1.65821183, 1.658095744, 1.657981659, 1.657869522, 1.657759285, 1.657650899, 1.657544319, 1.657439499, 1.657336397, 1.65723497, 1.657135178, 1.657036982, 1.656940344, 1.656845226, 1.656751594, 1.656659413, 1.656568649, 1.65647927, 1.656391244, 1.656304542, 1.656219133, 1.656134988, 1.65605208, 1.655970382, 1.655889868, 1.655810511, 1.655732287, 1.655655173, 1.655579143, 1.655504177, 1.655430251, 1.655357345, 1.655285437, 1.655214506, 1.655144534, 1.6550755, 1.655007387, 1.654940175, 1.654873847, 1.654808385, 1.654743774, 1.654679996, 1.654617035, 1.654554875, 1.654493503, 1.654432901, 1.654373057, 1.654313957, 1.654255585, 1.654197929, 1.654140976, 1.654084713, 1.654029128, 1.653974208, 1.653919942, 1.653866317, 1.653813324, 1.653760949, 1.653709184, 1.653658017, 1.653607437, 1.653557435, 1.653508002, 1.653459126, 1.6534108, 1.653363013, 1.653315758, 1.653269024, 1.653222803, 1.653177088, 1.653131869, 1.653087138, 1.653042889, 1.652999113, 1.652955802, 1.652912949, 1.652870547, 1.652828589, 1.652787068, 1.652745977, 1.65270531, 1.652665059, 1.652625219, 1.652585784, 1.652546746, 1.652508101 };
private static readonly double[] T_Table_05 = { 12.70620474, 4.30265273, 3.182446305, 2.776445105, 2.570581836, 2.446911851, 2.364624252, 2.306004135, 2.262157163, 2.228138852, 2.20098516, 2.17881283, 2.160368656, 2.144786688, 2.131449546, 2.119905299, 2.109815578, 2.10092204, 2.093024054, 2.085963447, 2.079613845, 2.073873068, 2.06865761, 2.063898562, 2.059538553, 2.055529439, 2.051830516, 2.048407142, 2.045229642, 2.042272456, 2.039513446, 2.036933343, 2.034515297, 2.032244509, 2.030107928, 2.028094001, 2.026192463, 2.024394164, 2.02269092, 2.02107539, 2.01954097, 2.018081703, 2.016692199, 2.015367574, 2.014103389, 2.012895599, 2.011740514, 2.010634758, 2.009575237, 2.008559112, 2.00758377, 2.006646805, 2.005745995, 2.004879288, 2.004044783, 2.003240719, 2.002465459, 2.001717484, 2.000995378, 2.000297822, 1.999623585, 1.998971517, 1.998340543, 1.997729654, 1.997137908, 1.996564419, 1.996008354, 1.995468931, 1.994945415, 1.994437112, 1.993943368, 1.993463567, 1.992997126, 1.992543495, 1.992102154, 1.99167261, 1.991254395, 1.990847069, 1.99045021, 1.990063421, 1.989686323, 1.989318557, 1.98895978, 1.988609667, 1.988267907, 1.987934206, 1.987608282, 1.987289865, 1.9869787, 1.986674541, 1.986377154, 1.986086317, 1.985801814, 1.985523442, 1.985251004, 1.984984312, 1.984723186, 1.984467455, 1.984216952, 1.983971519, 1.983731003, 1.983495259, 1.983264145, 1.983037526, 1.982815274, 1.982597262, 1.98238337, 1.982173483, 1.98196749, 1.981765282, 1.981566757, 1.981371815, 1.981180359, 1.980992298, 1.980807541, 1.980626002, 1.980447599, 1.980272249, 1.980099876, 1.979930405, 1.979763763, 1.979599878, 1.979438685, 1.979280117, 1.979124109, 1.978970602, 1.978819535, 1.97867085, 1.978524491, 1.978380405, 1.978238539, 1.978098842, 1.977961264, 1.977825758, 1.977692277, 1.977560777, 1.977431212, 1.977303542, 1.977177724, 1.97705372, 1.976931489, 1.976810994, 1.976692198, 1.976575066, 1.976459563, 1.976345655, 1.976233309, 1.976122494, 1.976013178, 1.975905331, 1.975798924, 1.975693928, 1.975590315, 1.975488058, 1.975387131, 1.975287508, 1.975189163, 1.975092073, 1.974996213, 1.97490156, 1.974808092, 1.974715786, 1.974624621, 1.974534576, 1.97444563, 1.974357764, 1.974270957, 1.974185191, 1.974100447, 1.974016708, 1.973933954, 1.973852169, 1.973771337, 1.97369144, 1.973612462, 1.973534388, 1.973457202, 1.973380889, 1.973305434, 1.973230823, 1.973157042, 1.973084077, 1.973011915, 1.972940542, 1.972869946, 1.972800114, 1.972731033, 1.972662692, 1.972595079, 1.972528182, 1.97246199, 1.972396491, 1.972331676, 1.972267533, 1.972204051, 1.972141222, 1.972079034, 1.972017478, 1.971956544, 1.971896224 };
private static readonly double[] T_Table_01 = { 63.65674116, 9.924843201, 5.84090931, 4.604094871, 4.032142984, 3.707428021, 3.499483297, 3.355387331, 3.249835542, 3.169272673, 3.105806516, 3.054539589, 3.012275839, 2.976842734, 2.946712883, 2.920781622, 2.89823052, 2.878440473, 2.860934606, 2.84533971, 2.831359558, 2.818756061, 2.807335684, 2.796939505, 2.787435814, 2.778714533, 2.770682957, 2.763262455, 2.756385904, 2.749995654, 2.744041919, 2.738481482, 2.733276642, 2.728394367, 2.723805589, 2.71948463, 2.715408722, 2.711557602, 2.707913184, 2.704459267, 2.701181304, 2.698066186, 2.695102079, 2.692278266, 2.689585019, 2.687013492, 2.684555618, 2.682204027, 2.679951974, 2.677793271, 2.675722234, 2.673733631, 2.671822636, 2.669984796, 2.668215988, 2.666512398, 2.664870482, 2.663286954, 2.661758752, 2.660283029, 2.658857127, 2.657478565, 2.656145025, 2.654854337, 2.653604469, 2.652393515, 2.651219685, 2.650081299, 2.648976774, 2.647904624, 2.646863444, 2.645851913, 2.644868782, 2.643912872, 2.642983067, 2.642078313, 2.641197611, 2.640340015, 2.639504627, 2.638690596, 2.637897113, 2.63712341, 2.636368757, 2.635632458, 2.634913852, 2.634212309, 2.633527229, 2.632858038, 2.632204191, 2.631565166, 2.630940463, 2.630329608, 2.629732145, 2.629147638, 2.628575671, 2.628015844, 2.627467774, 2.626931096, 2.626405457, 2.625890521, 2.625385965, 2.624891476, 2.624406758, 2.623931523, 2.623465496, 2.623008411, 2.622560015, 2.622120061, 2.621688313, 2.621264543, 2.620848534, 2.620440073, 2.620038957, 2.619644989, 2.619257981, 2.618877749, 2.618504116, 2.618136914, 2.617775976, 2.617421145, 2.617072266, 2.616729191, 2.616391776, 2.616059883, 2.615733377, 2.615412127, 2.615096008, 2.614784899, 2.61447868, 2.614177238, 2.613880461, 2.613588242, 2.613300477, 2.613017065, 2.612737908, 2.61246291, 2.61219198, 2.611925028, 2.611661966, 2.611402711, 2.611147181, 2.610895295, 2.610646976, 2.61040215, 2.610160742, 2.609922682, 2.609687901, 2.609456331, 2.609227907, 2.609002566, 2.608780245, 2.608560883, 2.608344423, 2.608130807, 2.60791998, 2.607711886, 2.607506474, 2.607303692, 2.607103489, 2.606905817, 2.606710628, 2.606517876, 2.606327515, 2.606139501, 2.605953791, 2.605770342, 2.605589114, 2.605410067, 2.605233162, 2.605058359, 2.604885623, 2.604714916, 2.604546204, 2.60437945, 2.604214622, 2.604051686, 2.60389061, 2.603731363, 2.603573912, 2.603418229, 2.603264282, 2.603112045, 2.602961487, 2.602812582, 2.602665303, 2.602519622, 2.602375515, 2.602232955, 2.602091918, 2.60195238, 2.601814317, 2.601677705, 2.601542523, 2.601408747, 2.601276355, 2.601145327, 2.601015642, 2.600887278, 2.600760216, 2.600634436 };
}
public enum Significance
{
P01,
P05,
P10,
P15,
P20,
P25
}
public class AverageSpeed
{
/// <summary>Average speed in units per second</summary>
public double Average { get; private set; }
public TimeSpan SlowWindow { get; }
public TimeSpan FastWindow { get; }
public Significance SlowSignificance { get; }
public Significance FastSignificance { get; }
private DateTime start;
private TimeSpan lastTime;
private double lastPosition = double.NaN;
private readonly record struct Point(TimeSpan Time, double Velocity);
private readonly LinkedList<Point> speeds = new();
private const int MAX_SPEEDS = 200;
public AverageSpeed() : this(TimeSpan.FromSeconds(15), Significance.P10, TimeSpan.FromSeconds(3), Significance.P01) { }
/// <param name="slowWindow">Total moving average time window</param>
/// <param name="slowSignificance">T-test significance level at which the newest speed will be considered different from the slow window's mean speed.</param>
/// <param name="fastWindow">A shorter moving window of the most resent speeds. The average speed in <paramref name="fastWindow"/> is compared to the average speed in the rest of <paramref name="slowWindow"/> to quickly detect large changes in speed.</param>
/// <param name="fastSignificance">T-test significance level at which the mean speed in <paramref name="fastWindow"/> will be considered different from the mean speed of the remainder of <paramref name="slowWindow"/>.</param>
public AverageSpeed(TimeSpan slowWindow, Significance slowSignificance, TimeSpan fastWindow, Significance fastSignificance)
{
SlowWindow = ArgumentValidator.EnsureGreaterThan(slowWindow, nameof(slowWindow), fastWindow);
FastWindow = ArgumentValidator.EnsureGreaterThan(fastWindow, nameof(fastWindow), TimeSpan.Zero);
SlowSignificance = slowSignificance;
FastSignificance = fastSignificance;
}
/// <summary>Add a new position to the moving average</summary>
public void AddPosition(double position)
{
var now = DateTime.UtcNow;
if (start == default)
start = now;
var time = now - start;
while (speeds.Count > MAX_SPEEDS || (speeds.Count > 2 && time - speeds.First!.Value.Time > SlowWindow))
speeds.RemoveFirst();
if (!double.IsNaN(lastPosition))
{
var newSpeed = (position - lastPosition) / (time - lastTime).TotalSeconds;
speeds.AddLast(new Point(time, newSpeed));
}
lastTime = time;
lastPosition = position;
Average = ComputeNextAverage();
}
private double ComputeNextAverage()
{
if (speeds.Count == 0)
return 0;
else if (speeds.Count == 1)
return speeds.Last!.Value.Velocity;
else
{
var n_newest = speeds.Count(s => s.Time > lastTime.Subtract(FastWindow));
var n_oldest = speeds.Count - n_newest;
if (speeds.Take(n_oldest).T_Test_2By(s => s.Velocity, speeds.TakeLast(n_newest), FastSignificance))
{
//Speeds in FastWindow are significantly different from reset of speeds in SlowWindow.
//Discard older speeds and keep only speeds in FastWindow
for (; n_oldest > 0; n_oldest--)
speeds.RemoveFirst();
return speeds.Average(s => s.Velocity);
}
else
return
speeds.T_Test_1By(s => s.Velocity, Average, SlowSignificance)
? speeds.Average(s => s.Velocity)
: Average;
}
}
}
+41 -44
View File
@@ -1,62 +1,59 @@
using System;
using Dinah.Core;
using Mpeg4Lib;
using System.IO;
using System.Text;
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
namespace AaxDecrypter;
public static class Cue
{
public static class Cue
{
public static string CreateContents(string filePath, ChapterInfo chapters)
{
var stringBuilder = new StringBuilder();
public static string CreateContents(string filePath, ChapterInfo chapters)
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
var startOffset = chapters.StartOffset;
var startOffset = chapters.StartOffset;
var trackCount = 0;
foreach (var c in chapters.Chapters)
{
var startTime = c.StartOffset - startOffset;
trackCount++;
var trackCount = 1;
foreach (var c in chapters.Chapters)
{
var startTime = c.StartOffset - startOffset;
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds / 1000d * 75)}");
}
stringBuilder.AppendLine($"TRACK {trackCount++} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds * 75d / 1000):D2}");
}
return stringBuilder.ToString();
}
return stringBuilder.ToString();
}
public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath)
=> UpdateFileName(cueFileInfo.FullName, audioFilePath);
public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath)
=> UpdateFileName(cueFileInfo.FullName, audioFilePath);
public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo)
=> UpdateFileName(cueFilePath, audioFileInfo.FullName);
public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo)
=> UpdateFileName(cueFilePath, audioFileInfo.FullName);
public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo)
=> UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName);
public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo)
=> UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName);
public static void UpdateFileName(string cueFilePath, string audioFilePath)
{
var cueContents = File.ReadAllLines(cueFilePath);
public static void UpdateFileName(string cueFilePath, string audioFilePath)
{
var cueContents = File.ReadAllLines(cueFilePath);
for (var i = 0; i < cueContents.Length; i++)
{
var line = cueContents[i];
if (!line.Trim().StartsWith("FILE") || !line.Contains(" "))
continue;
for (var i = 0; i < cueContents.Length; i++)
{
var line = cueContents[i];
if (!line.Trim().StartsWith("FILE") || !line.Contains(' '))
continue;
var fileTypeBegins = line.LastIndexOf(" ") + 1;
cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]);
break;
}
var fileTypeBegins = line.LastIndexOf(" ") + 1;
cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]);
break;
}
File.WriteAllLines(cueFilePath, cueContents);
}
File.WriteAllLines(cueFilePath, cueContents);
}
private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}";
}
private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}";
}
-30
View File
@@ -1,30 +0,0 @@
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public class DownloadOptions
{
public string DownloadUrl { get; }
public string UserAgent { get; }
public string AudibleKey { get; init; }
public string AudibleIV { get; init; }
public OutputFormat OutputFormat { get; init; }
public bool TrimOutputToChapterLength { get; init; }
public bool RetainEncryptedFile { get; init; }
public bool StripUnabridged { get; init; }
public bool CreateCueSheet { get; init; }
public ChapterInfo ChapterInfo { get; set; }
public NAudio.Lame.LameConfig LameConfig { get; set; }
public bool Downsample { get; set; }
public bool MatchSourceBitrate { get; set; }
public DownloadOptions(string downloadUrl, string userAgent)
{
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check for key/iv. unencrypted files do not have them
}
}
}
+54
View File
@@ -0,0 +1,54 @@
using AAXClean;
using Mpeg4Lib;
using System;
namespace AaxDecrypter;
public class KeyData
{
public byte[] KeyPart1 { get; }
public byte[]? KeyPart2 { get; }
public KeyData(byte[] keyPart1, byte[]? keyPart2 = null)
{
KeyPart1 = keyPart1;
KeyPart2 = keyPart2;
}
[Newtonsoft.Json.JsonConstructor]
public KeyData(string keyPart1, string? keyPart2 = null)
{
ArgumentNullException.ThrowIfNull(keyPart1, nameof(keyPart1));
KeyPart1 = Convert.FromHexString(keyPart1);
if (keyPart2 != null)
KeyPart2 = Convert.FromHexString(keyPart2);
}
}
public interface IDownloadOptions
{
event EventHandler<long> DownloadSpeedChanged;
string DownloadUrl { get; }
string UserAgent { get; }
KeyData[]? DecryptionKeys { get; }
TimeSpan RuntimeLength { get; }
OutputFormat OutputFormat { get; }
bool StripUnabridged { get; }
bool CreateCueSheet { get; }
long DownloadSpeedBps { get; }
ChapterInfo ChapterInfo { get; }
bool FixupFile { get; }
string? AudibleProductId { get; }
string? Title { get; }
string? Subtitle { get; }
string? Publisher { get; }
string? Language { get; }
string? SeriesName { get; }
string? SeriesNumber { get; }
NAudio.Lame.LameConfig? LameConfig { get; }
bool Downsample { get; }
bool MatchSourceBitrate { get; }
bool MoveMoovToBeginning { get; }
string GetMultipartTitle(MultiConvertFileProperties props);
public FileType? InputType { get; }
}
+71
View File
@@ -0,0 +1,71 @@
using AAXClean.Codecs;
using Mpeg4Lib;
using NAudio.Lame;
using System;
using System.Linq;
namespace AaxDecrypter;
public static class MpegUtil
{
private const string TagDomain = "com.pilabor.tone";
public static void ConfigureLameOptions(
Mpeg4File mp4File,
LameConfig lameConfig,
bool downsample,
bool matchSourceBitrate,
ChapterInfo? chapters)
{
double bitrateMultiple = 1;
if (mp4File.TimeScale < lameConfig.OutputSampleRate)
{
lameConfig.OutputSampleRate = mp4File.TimeScale;
}
else if (mp4File.TimeScale > lameConfig.OutputSampleRate)
{
bitrateMultiple *= (double)lameConfig.OutputSampleRate / mp4File.TimeScale;
}
if (mp4File.AudioChannels == 2)
{
if (downsample)
bitrateMultiple /= 2;
else
lameConfig.Mode = MPEGMode.Stereo;
}
if (matchSourceBitrate)
{
int kbps = (int)Math.Round(mp4File.AverageBitrate * bitrateMultiple / 1024);
if (lameConfig.VBR is null)
lameConfig.BitRate = kbps;
else if (lameConfig.VBR == VBRMode.ABR)
lameConfig.ABRRateKbps = kbps;
}
//Setup metadata tags
lameConfig.ID3 = mp4File.MetadataItems.ToIDTags();
if (mp4File.MetadataItems.AppleListBox.GetFreeformTagString(TagDomain, "SUBTITLE") is string subtitle)
lameConfig.ID3.Subtitle = subtitle;
if (chapters?.Count > 0)
{
var cue = Cue.CreateContents(lameConfig.ID3.Title + ".mp3", chapters);
lameConfig.ID3.UserDefinedText.Add("CUESHEET", cue);
}
//Copy over all other freeform tags
foreach (var t in mp4File.MetadataItems.AppleListBox.Tags.OfType<Mpeg4Lib.Boxes.FreeformTagBox>())
{
if (t.Name?.Name is string name &&
t.Mean?.ReverseDnsDomain is string domain &&
!lameConfig.ID3.UserDefinedText.ContainsKey(name) &&
mp4File.MetadataItems.AppleListBox.GetFreeformTagString(domain, name) is string tagStr &&
!string.IsNullOrWhiteSpace(tagStr))
lameConfig.ID3.UserDefinedText.Add(name, tagStr);
}
}
}
@@ -1,25 +1,12 @@
using System;
using System.IO;
using FileManager;
namespace AaxDecrypter
namespace AaxDecrypter;
public class MultiConvertFileProperties
{
public class MultiConvertFileProperties
{
public string OutputFileName { get; set; }
public int PartsPosition { get; set; }
public int PartsTotal { get; set; }
public string Title { get; set; }
public static string DefaultMultipartFilename(MultiConvertFileProperties multiConvertFileProperties)
{
var template = Path.ChangeExtension(multiConvertFileProperties.OutputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(multiConvertFileProperties.OutputFileName);
var fileNamingTemplate = new FileNamingTemplate(template) { IllegalCharacterReplacements = " " };
fileNamingTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(multiConvertFileProperties.PartsPosition, multiConvertFileProperties.PartsTotal));
fileNamingTemplate.AddParameterReplacement("title", multiConvertFileProperties.Title ?? "");
return fileNamingTemplate.GetFilePath();
}
}
public required string OutputFileName { get; set; }
public int PartsPosition { get; set; }
public int PartsTotal { get; set; }
public string? Title { get; set; }
public DateTime FileDate { get; } = DateTime.Now;
}
+377 -394
View File
@@ -1,443 +1,426 @@
using Dinah.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace AaxDecrypter
namespace AaxDecrypter;
/// <summary>A resumable, simultaneous file downloader and reader. </summary>
public class NetworkFileStream : Stream, IUpdatable
{
/// <summary>
/// A <see cref="CookieContainer"/> for a single Uri.
/// </summary>
public class SingleUriCookieContainer : CookieContainer
public event EventHandler? Updated;
#region Public Properties
/// <summary> Location to save the downloaded data. </summary>
[JsonProperty(Required = Required.Always)]
public string SaveFilePath { get; }
/// <summary> Http(s) address of the file to download. </summary>
[JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; }
/// <summary> Http headers to be sent to the server with the request. </summary>
[JsonProperty(Required = Required.Always)]
public Dictionary<string, string> RequestHeaders { get; private set; }
/// <summary> The position in <see cref="SaveFilePath"/> that has been written and flushed to disk. </summary>
[JsonProperty(Required = Required.Always)]
public long WritePosition { get; private set; }
/// <summary> The total length of the <see cref="Uri"/> file to download. </summary>
[JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; }
[JsonIgnore]
public bool IsCancelled => _cancellationSource.IsCancellationRequested;
[JsonIgnore]
public Task? DownloadTask { get; private set; }
private long _speedLimit = 0;
/// <summary>bytes per second</summary>
public long SpeedLimit { get => _speedLimit; set => _speedLimit = value <= 0 ? 0 : Math.Max(value, MIN_BYTES_PER_SECOND); }
#endregion
#region Private Properties
private FileStream _writeFile { get; }
private FileStream _readFile { get; }
private CancellationTokenSource _cancellationSource { get; } = new();
private EventWaitHandle? _downloadedPiece { get; set; }
private DateTime NextUpdateTime { get; set; }
#endregion
#region Constants
//Download memory buffer size
private const int DOWNLOAD_BUFF_SZ = 8 * 1024;
//NetworkFileStream will flush all data in _writeFile to disk after every
//DATA_FLUSH_SZ bytes are written to the file stream.
private const int DATA_FLUSH_SZ = 1024 * 1024;
//Number of times per second the download rate is checked and throttled
private const int THROTTLE_FREQUENCY = 8;
//Minimum throttle rate. The minimum amount of data that can be throttled
//on each iteration of the download loop is DOWNLOAD_BUFF_SZ.
public const int MIN_BYTES_PER_SECOND = DOWNLOAD_BUFF_SZ * THROTTLE_FREQUENCY;
#endregion
#region Constructor
/// <summary> A resumable, simultaneous file downloader and reader. </summary>
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
/// <param name="uri">Http(s) address of the file to download.</param>
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary<string, string>? requestHeaders = null)
{
private Uri baseAddress;
public Uri Uri
SaveFilePath = ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
Uri = ArgumentValidator.EnsureNotNull(uri, nameof(uri));
WritePosition = ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
RequestHeaders = requestHeaders ?? new();
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{
get => baseAddress;
set
{
baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
}
Position = WritePosition
};
if (_writeFile.Length < WritePosition)
{
_writeFile.Dispose();
throw new InvalidDataException($"{SaveFilePath} file length is shorter than {WritePosition}");
}
public CookieCollection GetCookies()
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
SetUriForSameFile(uri);
}
#endregion
#region Downloader
/// <summary> Update the <see cref="Dinah.Core.IO.JsonFilePersister{T}"/>. </summary>
private void OnUpdate(bool waitForWrite = false)
{
try
{
return GetCookies(Uri);
if (waitForWrite || DateTime.UtcNow > NextUpdateTime)
{
Updated?.Invoke(this, EventArgs.Empty);
//JsonFilePersister Will not allow update intervals shorter than 100 milliseconds
//If an update is called less than 100 ms since the last update, persister will
//sleep the thread until 100 ms has elapsed.
NextUpdateTime = DateTime.UtcNow.AddMilliseconds(110);
}
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "An error was encountered while saving the download progress to JSON");
}
}
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
public class NetworkFileStream : Stream, IUpdatable
/// <summary> Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/> </summary>
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
public void SetUriForSameFile(Uri uriToSameFile)
{
public event EventHandler Updated;
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
#region Public Properties
if (Path.GetFileName(uriToSameFile.LocalPath) != Path.GetFileName(Uri.LocalPath))
throw new ArgumentException($"New uri to the same file must have the same file name.");
if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (DownloadTask is not null)
throw new InvalidOperationException("Cannot change Uri after download has started.");
/// <summary>
/// Location to save the downloaded data.
/// </summary>
[JsonProperty(Required = Required.Always)]
public string SaveFilePath { get; }
Uri = uriToSameFile;
}
/// <summary>
/// Http(s) address of the file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; }
/// <summary>
/// All cookies set by caller or by the remote server.
/// </summary>
[JsonProperty(Required = Required.Always)]
public SingleUriCookieContainer CookieContainer { get; }
/// <summary>
/// Http headers to be sent to the server with the request.
/// </summary>
[JsonProperty(Required = Required.Always)]
public WebHeaderCollection RequestHeaders { get; private set; }
/// <summary>
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long WritePosition { get; private set; }
/// <summary>
/// The total length of the <see cref="Uri"/> file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; }
#endregion
#region Private Properties
private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; }
private FileStream _readFile { get; }
private Stream _networkStream { get; set; }
private bool hasBegunDownloading { get; set; }
public bool IsCancelled { get; private set; }
private EventWaitHandle downloadEnded { get; set; }
private EventWaitHandle downloadedPiece { get; set; }
#endregion
#region Constants
//Download buffer size
private const int DOWNLOAD_BUFF_SZ = 32 * 1024;
//NetworkFileStream will flush all data in _writeFile to disk after every
//DATA_FLUSH_SZ bytes are written to the file stream.
private const int DATA_FLUSH_SZ = 1024 * 1024;
#endregion
#region Constructor
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
/// <param name="uri">Http(s) address of the file to download.</param>
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param>
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
/// <summary> Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. </summary>
/// <returns>The downloader <see cref="Task"/></returns>
public async Task BeginDownloadingAsync()
{
if (ContentLength != 0 && WritePosition == ContentLength)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
SaveFilePath = saveFilePath;
Uri = uri;
WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new WebHeaderCollection();
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{
Position = WritePosition
};
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
SetUriForSameFile(uri);
}
#endregion
#region Downloader
/// <summary>
/// Update the <see cref="JsonFilePersister"/>.
/// </summary>
private void Update()
{
RequestHeaders = HttpRequest.Headers;
Updated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
/// </summary>
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
public void SetUriForSameFile(Uri uriToSameFile)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (hasBegunDownloading)
throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile;
HttpRequest = WebRequest.CreateHttp(Uri);
HttpRequest.CookieContainer = CookieContainer;
HttpRequest.Headers = RequestHeaders;
//If NetworkFileStream is resuming, Header will already contain a range.
HttpRequest.Headers.Remove("Range");
HttpRequest.AddRange(WritePosition);
}
/// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary>
private void BeginDownloading()
{
downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
if (ContentLength != 0 && WritePosition == ContentLength)
{
hasBegunDownloading = true;
downloadEnded.Set();
return;
}
if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
var response = HttpRequest.GetResponse() as HttpWebResponse;
if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
//Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0-
if (WritePosition == 0)
ContentLength = response.ContentLength;
_networkStream = response.GetResponseStream();
downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background.
new Thread(() => DownloadFile())
{ IsBackground = true }
.Start();
hasBegunDownloading = true;
DownloadTask = Task.CompletedTask;
return;
}
/// <summary>
/// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>.
/// </summary>
private void DownloadFile()
{
var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
var buff = new byte[DOWNLOAD_BUFF_SZ];
//Initiate connection with the first request block and
//get the total content length before returning.
var client = new HttpClient();
var response = await RequestNextByteRangeAsync(client);
if (ContentLength != 0 && ContentLength != response.FileSize)
throw new WebException($"Content length of 0x{response.FileSize:X10} differs from partially downloaded content length of 0x{ContentLength:X10}");
ContentLength = response.FileSize;
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Hand off the client and the open request to the downloader to download and write data to file.
DownloadTask = Task.Run(() => DownloadLoopInternal(client, response), _cancellationSource.Token);
}
private async Task DownloadLoopInternal(HttpClient client, BlockResponse blockResponse)
{
try
{
long startPosition = WritePosition;
while (WritePosition < ContentLength && !IsCancelled)
{
try
{
await DownloadToFile(blockResponse);
}
catch (HttpIOException e)
when (e.HttpRequestError is HttpRequestError.ResponseEnded
&& WritePosition != startPosition
&& WritePosition < ContentLength && !IsCancelled)
{
Serilog.Log.Logger.Debug($"The download connection ended before the file completed downloading all 0x{ContentLength:X10} bytes");
//the download made *some* progress since the last attempt.
//Try again to complete the download from where it left off.
//Make sure to rewind file to last flush position.
_writeFile.Position = startPosition = WritePosition;
blockResponse.Dispose();
blockResponse = await RequestNextByteRangeAsync(client);
Serilog.Log.Logger.Debug($"Resuming the file download starting at position 0x{WritePosition:X10}.");
}
}
}
catch (Exception ex)
{
//Don't throw from DownloadTask.
//This task gets awaited in Dispose() and we don't want to have an unhandled exception there.
Serilog.Log.Error(ex, "An error was encountered during the download process.");
}
finally
{
_writeFile.Dispose();
blockResponse.Dispose();
client.Dispose();
}
}
private async Task<BlockResponse> RequestNextByteRangeAsync(HttpClient client)
{
using var request = new HttpRequestMessage(HttpMethod.Get, Uri);
//Just in case it snuck in the saved json (Issue #1232)
RequestHeaders.Remove("Range");
foreach (var header in RequestHeaders)
request.Headers.Add(header.Key, header.Value);
request.Headers.Add("Range", $"bytes={WritePosition}-");
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token);
if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
var totalSize = response.Content.Headers.ContentRange?.Length ??
throw new WebException("The response did not contain a total content length.");
var rangeSize = response.Content.Headers.ContentLength ??
throw new WebException($"The response did not contain a {nameof(response.Content.Headers.ContentLength)};");
return new BlockResponse(response, rangeSize, totalSize);
}
private readonly record struct BlockResponse(HttpResponseMessage Response, long BlockSize, long FileSize) : IDisposable
{
public void Dispose() => Response?.Dispose();
}
/// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
private async Task DownloadToFile(BlockResponse block)
{
var endPosition = WritePosition + block.BlockSize;
using var networkStream = await block.Response.Content.ReadAsStreamAsync(_cancellationSource.Token);
var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
var buff = new byte[DOWNLOAD_BUFF_SZ];
try
{
DateTime startTime = DateTime.UtcNow;
long bytesReadSinceThrottle = 0;
int bytesRead;
do
{
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead);
bytesRead = await networkStream.ReadAsync(buff, _cancellationSource.Token);
await _writeFile.WriteAsync(buff, 0, bytesRead, _cancellationSource.Token);
downloadPosition += bytesRead;
if (downloadPosition > nextFlush)
{
_writeFile.Flush();
await _writeFile.FlushAsync(_cancellationSource.Token);
WritePosition = downloadPosition;
Update();
OnUpdate();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set();
_downloadedPiece?.Set();
}
} while (downloadPosition < ContentLength && !IsCancelled);
#region throttle
_writeFile.Close();
_networkStream.Close();
bytesReadSinceThrottle += bytesRead;
if (SpeedLimit >= MIN_BYTES_PER_SECOND && bytesReadSinceThrottle > SpeedLimit / THROTTLE_FREQUENCY)
{
var delayMS = (int)(startTime.AddSeconds(1d / THROTTLE_FREQUENCY) - DateTime.UtcNow).TotalMilliseconds;
if (delayMS > 0)
await Task.Delay(delayMS, _cancellationSource.Token);
startTime = DateTime.UtcNow;
bytesReadSinceThrottle = 0;
}
#endregion
} while (downloadPosition < endPosition && !IsCancelled && bytesRead > 0);
await _writeFile.FlushAsync(_cancellationSource.Token);
WritePosition = downloadPosition;
Update();
downloadedPiece.Set();
downloadEnded.Set();
if (!IsCancelled && WritePosition < ContentLength)
if (!IsCancelled && WritePosition < endPosition)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
if (WritePosition > ContentLength)
if (WritePosition > endPosition)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
}
#endregion
#region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings()
catch (OperationCanceledException)
{
var settings = new JsonSerializerSettings();
settings.Converters.Add(new CookieContainerConverter());
settings.Converters.Add(new WebHeaderCollectionConverter());
return settings;
Serilog.Log.Information("Download was cancelled");
}
internal class CookieContainerConverter : JsonConverter
finally
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(SingleUriCookieContainer);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new SingleUriCookieContainer()
{
Uri = new Uri(jObj["Uri"].Value<string>()),
Capacity = jObj["Capacity"].Value<int>(),
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
};
var cookieList = jObj["Cookies"].ToList();
foreach (var cookie in cookieList)
{
result.Add(
new Cookie
{
Comment = cookie["Comment"].Value<string>(),
HttpOnly = cookie["HttpOnly"].Value<bool>(),
Discard = cookie["Discard"].Value<bool>(),
Domain = cookie["Domain"].Value<string>(),
Expired = cookie["Expired"].Value<bool>(),
Expires = cookie["Expires"].Value<DateTime>(),
Name = cookie["Name"].Value<string>(),
Path = cookie["Path"].Value<string>(),
Port = cookie["Port"].Value<string>(),
Secure = cookie["Secure"].Value<bool>(),
Value = cookie["Value"].Value<string>(),
Version = cookie["Version"].Value<int>(),
});
}
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var cookies = value as SingleUriCookieContainer;
var obj = (JObject)JToken.FromObject(value);
var container = cookies.GetCookies();
var propertyNames = container.Select(c => JToken.FromObject(c));
obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
obj.WriteTo(writer);
}
}
internal class WebHeaderCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(WebHeaderCollection);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new WebHeaderCollection();
foreach (var kvp in jObj)
result.Add(kvp.Key, kvp.Value.Value<string>());
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var jObj = new JObject();
var type = value.GetType();
var headers = value as WebHeaderCollection;
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
jObj.Add(jHeaders);
jObj.WriteTo(writer);
}
}
#endregion
#region Download Stream Reader
[JsonIgnore]
public override bool CanRead => true;
[JsonIgnore]
public override bool CanSeek => true;
[JsonIgnore]
public override bool CanWrite => false;
[JsonIgnore]
public override long Length
{
get
{
if (!hasBegunDownloading)
BeginDownloading();
return ContentLength;
}
}
[JsonIgnore]
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
[JsonIgnore]
public override bool CanTimeout => false;
[JsonIgnore]
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
[JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
public override void Flush() => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
public override int Read(byte[] buffer, int offset, int count)
{
if (!hasBegunDownloading)
BeginDownloading();
var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead);
return _readFile.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
var newPosition = origin switch
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => ContentLength + offset,
_ => offset,
};
WaitToPosition(newPosition);
return _readFile.Position = newPosition;
}
/// <summary>
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
{
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
}
public override void Close()
{
IsCancelled = true;
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
_readFile.Close();
_writeFile.Close();
_networkStream?.Close();
Update();
}
#endregion
~NetworkFileStream()
{
downloadEnded?.Close();
downloadedPiece?.Close();
_downloadedPiece?.Set();
OnUpdate(waitForWrite: true);
}
}
#endregion
#region Download Stream Reader
[JsonIgnore]
public override bool CanRead => _readFile.CanRead;
[JsonIgnore]
public override bool CanSeek => _readFile.CanSeek;
[JsonIgnore]
public override bool CanWrite => false;
[JsonIgnore]
public override long Length
{
get
{
if (DownloadTask is null)
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
return ContentLength;
}
}
[JsonIgnore]
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
[JsonIgnore]
public override bool CanTimeout => false;
[JsonIgnore]
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
[JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
public override void Flush() => throw new InvalidOperationException();
public override void SetLength(long value) => throw new InvalidOperationException();
public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException();
public override int Read(byte[] buffer, int offset, int count)
{
if (DownloadTask is null)
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead);
return IsCancelled ? 0 : _readFile.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
var newPosition = origin switch
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => ContentLength + offset,
_ => offset,
};
WaitToPosition(newPosition);
return _readFile.Position = newPosition;
}
/// <summary>Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns. </summary>
/// <param name="requiredPosition">The minimum required flushed data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
{
while (WritePosition < requiredPosition
&& DownloadTask?.IsCompleted is false
&& !IsCancelled)
{
_downloadedPiece?.WaitOne(50);
}
}
private bool disposed = false;
/*
* https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.dispose?view=net-7.0
*
* In derived classes, do not override the Close() method, instead, put all of the
* Stream cleanup logic in the Dispose(Boolean) method.
*/
protected override void Dispose(bool disposing)
{
if (disposing && !Interlocked.CompareExchange(ref disposed, true, false))
{
_cancellationSource.Cancel();
DownloadTask?.GetAwaiter().GetResult();
_downloadedPiece?.Dispose();
_cancellationSource?.Dispose();
_readFile.Dispose();
_writeFile.Dispose();
OnUpdate(waitForWrite: true);
}
base.Dispose(disposing);
}
#endregion
}
@@ -1,23 +1,24 @@
using Dinah.Core.IO;
using Newtonsoft.Json;
namespace AaxDecrypter
namespace AaxDecrypter;
internal class NetworkFileStreamPersister : JsonFilePersister<NetworkFileStream>
{
internal class NetworkFileStreamPersister : JsonFilePersister<NetworkFileStream>
{
/// <summary>Alias for Target </summary>
public NetworkFileStream NetworkFileStream => Target;
/// <summary>Alias for Target </summary>
public NetworkFileStream NetworkFileStream => Target;
/// <summary>uses path. create file if doesn't yet exist</summary>
public NetworkFileStreamPersister(NetworkFileStream networkFileStream, string path, string? jsonPath = null)
: base(networkFileStream, path, jsonPath) { }
/// <summary>uses path. create file if doesn't yet exist</summary>
public NetworkFileStreamPersister(NetworkFileStream networkFileStream, string path, string jsonPath = null)
: base(networkFileStream, path, jsonPath) { }
/// <summary>load from existing file</summary>
public NetworkFileStreamPersister(string path, string? jsonPath = null)
: base(path, jsonPath) { }
/// <summary>load from existing file</summary>
public NetworkFileStreamPersister(string path, string jsonPath = null)
: base(path, jsonPath) { }
protected override JsonSerializerSettings GetSerializerSettings() => NetworkFileStream.GetJsonSerializerSettings();
}
protected override void Dispose(bool disposing)
{
if (disposing)
NetworkFileStream?.Dispose();
base.Dispose(disposing);
}
}
+16
View File
@@ -0,0 +1,16 @@
using FileManager;
namespace AaxDecrypter;
public record TempFile
{
public LongPath FilePath { get; init; }
public string Extension { get; }
public MultiConvertFileProperties? PartProperties { get; init; }
public TempFile(LongPath filePath, string? extension = null)
{
FilePath = filePath;
extension ??= System.IO.Path.GetExtension(filePath);
Extension = FileUtility.GetStandardizedExtension(extension).ToLowerInvariant();
}
}
@@ -1,76 +1,33 @@
using System;
using System.Threading;
using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner;
using FileManager;
using FileManager;
using System.Threading.Tasks;
namespace AaxDecrypter
namespace AaxDecrypter;
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
{
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
protected override long InputFilePosition => InputFileStream.WritePosition;
public UnencryptedAudiobookDownloader(string outDirectory, string cacheDirectory, IDownloadOptions dlLic)
: base(outDirectory, cacheDirectory, dlLic)
{
protected override StepSequence Steps { get; }
AsyncSteps.Name = "Download Unencrypted Audiobook";
AsyncSteps["Step 1: Download Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
AsyncSteps["Step 2: Create Cue"] = Step_CreateCueAsync;
}
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic)
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{
await (InputFileStream.DownloadTask ?? Task.CompletedTask);
if (IsCanceled)
return false;
else
{
Steps = new StepSequence
{
Name = "Download Mp3 Audiobook",
["Step 1: Get Mp3 Metadata"] = Step_GetMetadata,
["Step 2: Download Audiobook"] = Step_DownloadAudiobookAsSingleFile,
["Step 3: Create Cue"] = Step_CreateCue,
["Step 4: Cleanup"] = Step_Cleanup,
};
}
public override void Cancel()
{
IsCanceled = true;
CloseInputFileStream();
}
protected bool Step_GetMetadata()
{
OnRetrievedCoverArt(null);
return !IsCanceled;
}
private bool Step_DownloadAudiobookAsSingleFile()
{
DateTime startTime = DateTime.Now;
// MUST put InputFileStream.Length first, because it starts background downloader.
while (InputFileStream.Length > InputFileStream.WritePosition && !InputFileStream.IsCancelled)
{
var rate = InputFileStream.WritePosition / (DateTime.Now - startTime).TotalSeconds;
var estTimeRemaining = (InputFileStream.Length - InputFileStream.WritePosition) / rate;
if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
var progressPercent = (double)InputFileStream.WritePosition / InputFileStream.Length;
OnDecryptProgressUpdate(
new DownloadProgress
{
ProgressPercentage = 100 * progressPercent,
BytesReceived = (long)(InputFileStream.Length * progressPercent),
TotalBytesToReceive = InputFileStream.Length
});
Thread.Sleep(200);
}
CloseInputFileStream();
var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName);
SetOutputFileName(realOutputFileName);
OnFileCreated(realOutputFileName);
return !IsCanceled;
FinalizeDownload();
var tempFile = GetNewTempFilePath(DownloadOptions.OutputFormat.ToString());
FileUtility.SaferMove(InputFileStream.SaveFilePath, tempFile.FilePath);
OnTempFileCreated(tempFile);
return true;
}
}
}
-4
View File
@@ -1,4 +0,0 @@
{
"//": "https://github.com/BalassaMarton/MSBump",
BumpRevision: true
}
+17 -12
View File
@@ -1,21 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>7.8.0.1</Version>
<TargetFramework>net10.0</TargetFramework>
<Version>13.4.9</Version>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSBump" Version="2.3.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Octokit" Version="0.51.0" />
<PackageReference Include="Octokit" Version="14.0.0" />
<!-- Do not remove unused Serilog.Sinks -->
<!-- Only File sink is currently used. By user request (June 2024) others packages are included for experimental use. -->
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
</Project>
File diff suppressed because it is too large. Load diff
+245 -249
View File
@@ -1,271 +1,267 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using Dinah.Core;
using LibationFileManager;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
namespace AppScaffolding
namespace AppScaffolding;
/// <summary>
///
///
/// directly manipulates settings files without going through domain logic.
///
/// for migrations only. use with caution.
///
///
/// </summary>
public static class UNSAFE_MigrationHelper
{
/// <summary>
///
///
/// directly manipulates settings files without going through domain logic.
///
/// for migrations only. use with caution.
///
///
/// </summary>
internal static class UNSAFE_MigrationHelper
public static string? SettingsDirectory
=> !APPSETTINGS_TryGet(LibationFiles.LIBATION_FILES_KEY, out var value) || value is null
? null
: value;
#region appsettings.json
public static bool APPSETTINGS_TryGet(string key, out string? value)
{
#region appsettings.json
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json");
bool success = false;
JToken? val = null;
public static bool APPSETTINGS_Json_Exists => File.Exists(APPSETTINGS_JSON);
process_APPSETTINGS_Json(jObj => success = jObj.TryGetValue(key, out val), false);
public static bool APPSETTINGS_TryGet(string key, out string value)
value = success ? val?.Value<string>() : null;
return success;
}
/// <summary>only insert if not exists</summary>
public static void APPSETTINGS_Insert(string key, string value)
=> process_APPSETTINGS_Json(jObj => jObj.TryAdd(key, value));
/// <summary>only update if exists</summary>
public static void APPSETTINGS_Update(string key, string value)
=> process_APPSETTINGS_Json(jObj =>
{
bool success = false;
JToken val = null;
if (jObj.ContainsKey(key))
jObj[key] = value;
});
process_APPSETTINGS_Json(jObj => success = jObj.TryGetValue(key, out val), false);
/// <summary>only delete if exists</summary>
public static void APPSETTINGS_Delete(string key)
=> process_APPSETTINGS_Json(jObj =>
{
if (jObj.ContainsKey(key))
jObj.Remove(key);
});
value = success ? val.Value<string>() : null;
return success;
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
private static void process_APPSETTINGS_Json(Action<JObject> action, bool save = true)
{
if (Configuration.Instance.LibationFiles.AppsettingsJsonFile is not string appSettingsFile)
return;
var startingContents = File.ReadAllText(appSettingsFile);
JObject jObj;
try
{
jObj = JObject.Parse(startingContents);
}
catch
{
return;
}
/// <summary>only insert if not exists</summary>
public static void APPSETTINGS_Insert(string key, string value)
=> process_APPSETTINGS_Json(jObj => jObj.TryAdd(key, value));
action(jObj);
/// <summary>only update if exists</summary>
public static void APPSETTINGS_Update(string key, string value)
=> process_APPSETTINGS_Json(jObj => {
if (jObj.ContainsKey(key))
jObj[key] = value;
});
if (!save)
return;
/// <summary>only delete if exists</summary>
public static void APPSETTINGS_Delete(string key)
=> process_APPSETTINGS_Json(jObj => {
if (jObj.ContainsKey(key))
jObj.Remove(key);
});
// only save if different
var endingContents_indented = jObj.ToString(Formatting.Indented);
var endingContents_compact = jObj.ToString(Formatting.None);
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
return;
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
private static void process_APPSETTINGS_Json(Action<JObject> action, bool save = true)
File.WriteAllText(Configuration.Instance.LibationFiles.AppsettingsJsonFile, endingContents_indented);
System.Threading.Thread.Sleep(100);
}
#endregion
#region Settings.json
public static string? SettingsJsonPath => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LibationFiles.SETTINGS_JSON);
public static bool Settings_TryGet(string key, out string? value)
{
bool success = false;
JToken? val = null;
process_SettingsJson(jObj => success = jObj.TryGetValue(key, out val), false);
value = success ? val?.Value<string>() : null;
return success;
}
public static bool Settings_JsonPathIsType(string jsonPath, JTokenType jTokenType)
{
JToken? val = null;
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
return val?.Type == jTokenType;
}
public static bool Settings_TryGetFromJsonPath(string jsonPath, out string? value)
{
JToken? val = null;
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
if (val?.Type == JTokenType.String)
{
// only insert if not exists
if (!APPSETTINGS_Json_Exists)
return;
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
JObject jObj;
try
{
jObj = JObject.Parse(startingContents);
}
catch
{
return;
}
action(jObj);
if (!save)
return;
// only save if different
var endingContents_indented = jObj.ToString(Formatting.Indented);
var endingContents_compact = jObj.ToString(Formatting.None);
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
return;
File.WriteAllText(APPSETTINGS_JSON, endingContents_indented);
System.Threading.Thread.Sleep(100);
}
#endregion
#region Settings.json
public const string LIBATION_FILES_KEY = "LibationFiles";
private const string SETTINGS_JSON = "Settings.json";
public static string SettingsJsonPath
{
get
{
var success = APPSETTINGS_TryGet(LIBATION_FILES_KEY, out var value);
return !success || value is null ? null : Path.Combine(value, SETTINGS_JSON);
}
}
public static bool SettingsJson_Exists => SettingsJsonPath is not null && File.Exists(SettingsJsonPath);
public static bool Settings_TryGet(string key, out string value)
{
bool success = false;
JToken val = null;
process_SettingsJson(jObj => success = jObj.TryGetValue(key, out val), false);
value = success ? val.Value<string>() : null;
return success;
}
public static bool Settings_JsonPathIsType(string jsonPath, JTokenType jTokenType)
{
JToken val = null;
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
return val?.Type == jTokenType;
}
public static bool Settings_TryGetFromJsonPath(string jsonPath, out string value)
{
JToken val = null;
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
if (val?.Type == JTokenType.String)
{
value = val.Value<string>();
return true;
}
else
{
value = null;
return false;
}
}
public static void Settings_SetWithJsonPath(string jsonPath, string propertyName, string newValue)
{
if (!Settings_TryGetFromJsonPath($"{jsonPath}.{propertyName}", out _))
return;
process_SettingsJson(jObj =>
{
var token = jObj.SelectToken(jsonPath);
if (token is null
|| token is not JObject o
|| o[propertyName] is null)
return;
var oldValue = token.Value<string>(propertyName);
if (oldValue != newValue)
token[propertyName] = newValue;
});
}
public static bool Settings_TryGetArrayLength(string jsonPath, out int length)
{
length = 0;
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return false;
JArray array = null;
process_SettingsJson(jObj => array = (JArray)jObj.SelectToken(jsonPath));
length = array.Count;
value = val.Value<string>();
return true;
}
public static void Settings_AddToArray(string jsonPath, string newValue)
else
{
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return;
process_SettingsJson(jObj =>
{
var array = (JArray)jObj.SelectToken(jsonPath);
array.Add(newValue);
});
value = null;
return false;
}
/// <summary>Do not add if already exists</summary>
public static void Settings_AddUniqueToArray(string arrayPath, string newValue)
{
if (!Settings_TryGetArrayLength(arrayPath, out var qty))
return;
for (var i = 0; i < qty; i++)
{
var exists = Settings_TryGetFromJsonPath($"{arrayPath}[{i}]", out var value);
if (exists && value == newValue)
return;
}
Settings_AddToArray(arrayPath, newValue);
}
/// <summary>only remove if not exists</summary>
public static void Settings_RemoveFromArray(string jsonPath, int position)
{
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return;
process_SettingsJson(jObj =>
{
var array = (JArray)jObj.SelectToken(jsonPath);
if (position < array.Count)
array.RemoveAt(position);
});
}
/// <summary>only insert if not exists</summary>
public static void Settings_Insert(string key, string value)
=> process_SettingsJson(jObj => jObj.TryAdd(key, value));
/// <summary>only update if exists</summary>
public static void Settings_Update(string key, string value)
=> process_SettingsJson(jObj => {
if (jObj.ContainsKey(key))
jObj[key] = value;
});
/// <summary>only delete if exists</summary>
public static void Settings_Delete(string key)
=> process_SettingsJson(jObj => {
if (jObj.ContainsKey(key))
jObj.Remove(key);
});
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
private static void process_SettingsJson(Action<JObject> action, bool save = true)
{
// only insert if not exists
if (!SettingsJson_Exists)
return;
var startingContents = File.ReadAllText(SettingsJsonPath);
JObject jObj;
try
{
jObj = JObject.Parse(startingContents);
}
catch
{
return;
}
action(jObj);
if (!save)
return;
// only save if different
var endingContents_indented = jObj.ToString(Formatting.Indented);
var endingContents_compact = jObj.ToString(Formatting.None);
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
return;
File.WriteAllText(SettingsJsonPath, endingContents_indented);
System.Threading.Thread.Sleep(100);
}
#endregion
}
public static void Settings_SetWithJsonPath(string jsonPath, string propertyName, string newValue)
{
if (!Settings_TryGetFromJsonPath($"{jsonPath}.{propertyName}", out _))
return;
process_SettingsJson(jObj =>
{
var token = jObj.SelectToken(jsonPath);
if (token is null
|| token is not JObject o
|| o[propertyName] is null)
return;
var oldValue = token.Value<string>(propertyName);
if (oldValue != newValue)
token[propertyName] = newValue;
});
}
public static bool Settings_TryGetArrayLength(string jsonPath, out int length)
{
length = 0;
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return false;
JArray? array = null;
process_SettingsJson(jObj => array = jObj.SelectToken(jsonPath) as JArray);
length = array?.Count ?? 0;
return true;
}
public static void Settings_AddToArray(string jsonPath, string newValue)
{
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return;
process_SettingsJson(jObj =>
{
(jObj.SelectToken(jsonPath) as JArray)?.Add(newValue);
});
}
/// <summary>Do not add if already exists</summary>
public static void Settings_AddUniqueToArray(string arrayPath, string newValue)
{
if (!Settings_TryGetArrayLength(arrayPath, out var qty))
return;
for (var i = 0; i < qty; i++)
{
var exists = Settings_TryGetFromJsonPath($"{arrayPath}[{i}]", out var value);
if (exists && value == newValue)
return;
}
Settings_AddToArray(arrayPath, newValue);
}
/// <summary>only remove if not exists</summary>
public static void Settings_RemoveFromArray(string jsonPath, int position)
{
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return;
process_SettingsJson(jObj =>
{
if (jObj.SelectToken(jsonPath) is JArray array && position < array.Count)
array.RemoveAt(position);
});
}
/// <summary>only insert if not exists</summary>
public static void Settings_Insert(string key, string value)
=> process_SettingsJson(jObj => jObj.TryAdd(key, value));
/// <summary>only update if exists</summary>
public static void Settings_Update(string key, string value)
=> process_SettingsJson(jObj =>
{
if (jObj.ContainsKey(key))
jObj[key] = value;
});
/// <summary>only delete if exists</summary>
public static void Settings_Delete(string key)
=> process_SettingsJson(jObj =>
{
if (jObj.ContainsKey(key))
jObj.Remove(key);
});
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
private static void process_SettingsJson(Action<JObject> action, bool save = true)
{
// only insert if not exists
if (!File.Exists(SettingsJsonPath))
return;
var startingContents = File.ReadAllText(SettingsJsonPath);
JObject jObj;
try
{
jObj = JObject.Parse(startingContents);
}
catch
{
return;
}
action(jObj);
if (!save)
return;
// only save if different
var endingContents_indented = jObj.ToString(Formatting.Indented);
var endingContents_compact = jObj.ToString(Formatting.None);
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
return;
File.WriteAllText(SettingsJsonPath, endingContents_indented);
System.Threading.Thread.Sleep(100);
}
#endregion
#region LibationContext.db
public const string LIBATION_CONTEXT = "LibationContext.db";
public static string? DatabaseFile => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LIBATION_CONTEXT);
public static bool DatabaseFile_Exists => DatabaseFile is not null && File.Exists(DatabaseFile);
#endregion
}
+39 -3
View File
@@ -1,6 +1,42 @@
using System;
using System;
using System.Text.RegularExpressions;
namespace AppScaffolding
namespace AppScaffolding;
/// <summary>Result of a version check: definitive up-to-date, update available, or unable to determine.</summary>
public enum VersionCheckOutcome
{
public record UpgradeProperties(string ZipUrl, string HtmlUrl, string ZipName, Version LatestRelease);
UpToDate,
UpdateAvailable,
UnableToDetermine,
}
/// <summary>Result of checking for a new version. Use <see cref="Outcome"/> to show the right message; <see cref="UpgradeProperties"/> is set only when <see cref="Outcome"/> is <see cref="VersionCheckOutcome.UpdateAvailable"/>.</summary>
public record VersionCheckResult(VersionCheckOutcome Outcome, UpgradeProperties? UpgradeProperties = null);
public partial record UpgradeProperties
{
public string ZipUrl { get; }
public string HtmlUrl { get; }
public string ZipName { get; }
public Version LatestRelease { get; }
public string Notes { get; }
public UpgradeProperties(string zipUrl, string htmlUrl, string zipName, Version latestRelease, string notes)
{
ZipName = zipName;
HtmlUrl = htmlUrl;
ZipUrl = zipUrl;
LatestRelease = latestRelease;
var text = NoAppBlockRegex().Replace(notes, "");
text = LinkStripRegex().Replace(text, "$1");
Notes = text.Trim();
}
[GeneratedRegex(@"\[(.*)\]\(.*\)")]
private static partial Regex LinkStripRegex();
[GeneratedRegex(@"<!-- BEGIN NO-APP -->.*?<!-- END NO-APP -->", RegexOptions.Singleline)]
private static partial Regex NoAppBlockRegex();
}
@@ -1,17 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.2.1" />
<PackageReference Include="NPOI" Version="2.5.6" />
<PackageReference Include="CsvHelper" Version="33.1.0">
<PrivateAssets>compile;contentFiles;build;buildMultitargeting;buildTransitive;analyzers;native</PrivateAssets>
</PackageReference>
<PackageReference Include="ClosedXML" Version="0.105.0">
<PrivateAssets>compile;contentFiles;build;buildMultitargeting;buildTransitive;analyzers;native</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DtoImporterService\DtoImporterService.csproj" />
<ProjectReference Include="..\LibationSearchEngine\LibationSearchEngine.csproj" />
<ProjectReference Include="..\DataLayer.Postgres\DataLayer.Postgres.csproj" />
<ProjectReference Include="..\DataLayer.Sqlite\DataLayer.Sqlite.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
</Project>
@@ -0,0 +1,76 @@
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ApplicationServices;
public class BulkSetDownloadStatus
{
private List<(string message, LiberatedStatus newStatus, IEnumerable<LibraryBook> LibraryBooks)> actionSets { get; } = new();
public int Count => actionSets.Count;
public IEnumerable<string> Messages => actionSets.Select(a => a.message);
public string AggregateMessage => $"Are you sure you want to set {Messages.Aggregate((a, b) => $"{a} and {b}")}?";
private readonly List<LibraryBook> _libraryBooks;
private readonly bool _setDownloaded;
private readonly bool _setNotDownloaded;
public BulkSetDownloadStatus(List<LibraryBook> libraryBooks, bool setDownloaded, bool setNotDownloaded)
{
_libraryBooks = libraryBooks;
_setDownloaded = setDownloaded;
_setNotDownloaded = setNotDownloaded;
}
public int Discover()
{
var bookExistsList = _libraryBooks
.Select(libraryBook => new
{
LibraryBook = libraryBook,
FileExists = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId) is not null
})
.ToList();
if (_setDownloaded)
{
var books2change = bookExistsList
.Where(a => a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
.Select(a => a.LibraryBook)
.ToList();
if (books2change.Any())
actionSets.Add((
$"{"book".PluralizeWithCount(books2change.Count)} to 'Downloaded'",
LiberatedStatus.Liberated,
books2change));
}
if (_setNotDownloaded)
{
var books2change = bookExistsList
.Where(a => !a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated)
.Select(a => a.LibraryBook)
.ToList();
if (books2change.Any())
actionSets.Add((
$"{"book".PluralizeWithCount(books2change.Count)} to 'Not Downloaded'",
LiberatedStatus.NotLiberated,
books2change));
}
return Count;
}
public async Task ExecuteAsync()
{
foreach (var a in actionSets)
await a.LibraryBooks.UpdateBookStatusAsync(a.newStatus);
}
}
+132 -13
View File
@@ -1,21 +1,140 @@
using System;
using System.Collections.Generic;
using DataLayer;
using DataLayer;
using LibationFileManager;
using Serilog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
namespace ApplicationServices
namespace ApplicationServices;
public static class DbContexts
{
public static class DbContexts
{
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
public static LibationContext GetContext()
=> LibationContext.Create(SqliteStorage.ConnectionString);
private static bool _sqliteDbValidated;
/// <summary>Use for full library querying. No lazy loading</summary>
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
private static readonly object _initialDatabaseStatisticsCaptureLock = new();
/// <summary>
/// True after initial DB statistics were read and either written to Serilog or stored for <see cref="TryEmitPendingInitialDatabaseStatistics"/>.
/// False if capture has not run yet or the last attempt threw (a later <see cref="GetContext"/> may retry).
/// </summary>
private static bool _initialDatabaseStatisticsCaptured;
/// <summary>Shape of the initial DB statistics log event; edit here only when changing what is logged.</summary>
private sealed class InitialDatabaseStatistics
{
public required int LibraryBooksNotInTrash { get; init; }
public required int LibraryBooksInTrash { get; init; }
public required int BookRecords { get; init; }
}
private static InitialDatabaseStatistics? _pendingInitialDbStats;
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
public static LibationContext GetContext()
{
var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString)
? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString)
: LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString);
try
{
using var context = GetContext();
return context.GetLibrary_Flat_NoTracking();
LibationContextFactory.ApplyMigrations(
context,
string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString) ? SqliteStorage.DatabasePath : null);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("Libation cannot write its SQLite database", StringComparison.Ordinal))
{
Log.Error(
ex,
"SQLite migrations failed (read-only or blocked). LibationFiles={LibationFiles} DatabasePath={DatabasePath} AppsettingsJson={AppsettingsJson}",
Configuration.Instance.LibationFiles.Location,
SqliteStorage.DatabasePath,
Configuration.Instance.LibationFiles.AppsettingsJsonFile ?? "(null, LIBATION_FILES_DIR may be set)");
throw;
}
// Validate SQLite DB file was created and is accessible (once per process; OS may delay availability)
if (!_sqliteDbValidated && string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString))
{
EssentialFileValidator.ValidateCreatedAndReport(SqliteStorage.DatabasePath);
_sqliteDbValidated = true;
}
TryCaptureInitialDatabaseStatistics(context);
return context;
}
private static void TryCaptureInitialDatabaseStatistics(LibationContext context)
{
lock (_initialDatabaseStatisticsCaptureLock)
{
if (_initialDatabaseStatisticsCaptured)
return;
try
{
var (notInTrash, inTrash) = context.GetLibraryBookCountsByTrashFlag();
var bookRecords = context.GetBookCount();
var stats = new InitialDatabaseStatistics
{
LibraryBooksNotInTrash = notInTrash,
LibraryBooksInTrash = inTrash,
BookRecords = bookRecords,
};
if (Configuration.Instance.SerilogInitialized)
LogInitialDatabaseStatistics(stats);
else
_pendingInitialDbStats = stats;
_initialDatabaseStatisticsCaptured = true;
}
catch (Exception ex)
{
if (Configuration.Instance.SerilogInitialized)
Log.Warning(ex, "Could not capture initial database statistics");
}
}
}
/// <summary>
/// Writes initial DB statistics that were captured before Serilog was configured (e.g. WinForms early library load).
/// Call once after <see cref="Configuration.ConfigureLogging"/>.
/// </summary>
public static void TryEmitPendingInitialDatabaseStatistics()
{
var pending = Interlocked.Exchange(ref _pendingInitialDbStats, null);
if (pending is not null)
LogInitialDatabaseStatistics(pending);
}
private static void LogInitialDatabaseStatistics(InitialDatabaseStatistics stats) =>
Log.Logger.Information("Initial database statistics. {@DbStats}", stats);
/// <summary>Use for full library querying. No lazy loading</summary>
public static List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)
{
using var context = GetContext();
return context.GetLibrary_Flat_NoTracking(includeParents);
}
public static List<LibraryBook> GetUnliberated_Flat_NoTracking()
{
using var context = GetContext();
return context.GetUnLiberated_Flat_NoTracking();
}
public static List<LibraryBook> GetDeletedLibraryBooks()
{
using var context = GetContext();
return context.GetDeletedLibraryBooks();
}
public static LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true, string? account = null)
{
using var context = GetContext();
return context.GetLibraryBook_Flat_NoTracking(productId, caseSensative, account);
}
}
+141
View File
@@ -0,0 +1,141 @@
using CsvHelper.Configuration.Attributes;
using DataLayer;
using Newtonsoft.Json;
using System;
using System.Linq;
namespace ApplicationServices;
internal class ExportDto(LibraryBook libBook)
{
[Name("Account")]
public string Account { get; } = libBook.Account;
[Name("Date Added to library")]
public DateTime DateAdded { get; } = libBook.DateAdded;
[Name("Is Audible Plus?")]
public bool IsAudiblePlus { get; } = libBook.IsAudiblePlus;
[Name("Absent from last scan?")]
public bool AbsentFromLastScan { get; } = libBook.AbsentFromLastScan;
[Name("Audible Product Id")]
public string AudibleProductId { get; } = libBook.Book.AudibleProductId;
[Name("Locale")]
public string Locale { get; } = libBook.Book.Locale;
[Name("Title")]
public string Title { get; } = libBook.Book.Title;
[Name("Subtitle")]
public string Subtitle { get; } = libBook.Book.Subtitle;
[Name("Authors")]
public string AuthorNames { get; } = libBook.Book.AuthorNames;
[Name("Narrators")]
public string NarratorNames { get; } = libBook.Book.NarratorNames;
[Name("Length In Minutes")]
public int LengthInMinutes { get; } = libBook.Book.LengthInMinutes;
[Name("Description")]
public string Description { get; } = libBook.Book.Description;
[Name("Publisher")]
public string? Publisher { get; } = libBook.Book.Publisher;
[Name("Has PDF")]
public bool HasPdf { get; } = libBook.Book.HasPdf;
[Name("Series Names")]
public string SeriesNames { get; } = libBook.Book.SeriesNames();
[Name("Series Order")]
public string SeriesOrder { get; } = libBook.Book.SeriesLink?.Any() is true ? string.Join(", ", libBook.Book.SeriesLink.Select(sl => $"{sl.Order} : {sl.Series.Name}")) : "";
[Name("Community Rating: Overall")]
public float? CommunityRatingOverall { get; } = ZeroIsNull(libBook.Book.Rating?.OverallRating);
[Name("Community Rating: Performance")]
public float? CommunityRatingPerformance { get; } = ZeroIsNull(libBook.Book.Rating?.PerformanceRating);
[Name("Community Rating: Story")]
public float? CommunityRatingStory { get; } = ZeroIsNull(libBook.Book.Rating?.StoryRating);
[Name("Cover Id")]
public string? PictureId { get; } = libBook.Book.PictureId;
[Name("Cover Id Large")]
public string? PictureLarge { get; } = libBook.Book.PictureLarge;
[Name("Is Abridged?")]
public bool IsAbridged { get; } = libBook.Book.IsAbridged;
[Name("Date Published")]
public DateTime? DatePublished { get; } = libBook.Book.DatePublished;
[Name("Categories")]
public string CategoriesNames { get; } = string.Join("; ", libBook.Book.LowestCategoryNames());
[Name("My Rating: Overall")]
public float? MyRatingOverall { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.OverallRating);
[Name("My Rating: Performance")]
public float? MyRatingPerformance { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.PerformanceRating);
[Name("My Rating: Story")]
public float? MyRatingStory { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.StoryRating);
[Name("My Libation Tags")]
public string MyLibationTags { get; } = libBook.Book.UserDefinedItem.Tags;
[Name("Book Liberated Status")]
public string BookStatus { get; } = libBook.Book.UserDefinedItem.BookStatus.ToString();
[Name("PDF Liberated Status")]
public string? PdfStatus { get; } = libBook.Book.UserDefinedItem.PdfStatus.ToString();
[Name("Content Type")]
public string ContentType { get; } = libBook.Book.ContentType.ToString();
[Name("Language")]
public string? Language { get; } = libBook.Book.Language;
[Name("Last Downloaded")]
public DateTime? LastDownloaded { get; } = libBook.Book.UserDefinedItem.LastDownloaded;
[Name("Last Downloaded Version")]
public string? LastDownloadedVersion { get; } = libBook.Book.UserDefinedItem.LastDownloadedVersion?.ToString();
[Name("Is Finished?")]
public bool IsFinished { get; } = libBook.Book.UserDefinedItem.IsFinished;
[Name("Is Spatial?")]
public bool IsSpatial { get; } = libBook.Book.IsSpatial;
[Name("Included Until")]
public DateTime? IncludedUntil { get; } = libBook.IncludedUntil;
[Name("Last Downloaded File Version")]
public string? LastDownloadedFileVersion { get; } = libBook.Book.UserDefinedItem.LastDownloadedFileVersion;
[Ignore /* csv ignore */]
public AudioFormat? LastDownloadedFormat { get; } = libBook.Book.UserDefinedItem.LastDownloadedFormat;
[Name("Last Downloaded Codec"), JsonIgnore]
public string CodecString => LastDownloadedFormat?.CodecString ?? "";
[Name("Last Downloaded Sample rate"), JsonIgnore]
public int? SampleRate => LastDownloadedFormat?.SampleRate;
[Name("Last Downloaded Audio Channels"), JsonIgnore]
public int? ChannelCount => LastDownloadedFormat?.ChannelCount;
[Name("Last Downloaded Bitrate"), JsonIgnore]
public int? BitRate => LastDownloadedFormat?.BitRate;
private static float? ZeroIsNull(float? value) => value is 0 ? null : value;
}
@@ -0,0 +1,8 @@
using LibationSearchEngine;
namespace ApplicationServices;
public interface ISearchEngine
{
SearchResultSet? GetSearchResultSet(string? searchString);
}
+552 -220
View File
@@ -1,64 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi;
using AudibleUtilities;
using DataLayer;
using Dinah.Core;
using Dinah.Core.Logging;
using DtoImporterService;
using FileManager;
using LibationFileManager;
using Newtonsoft.Json.Linq;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static DtoImporterService.PerfLogger;
namespace ApplicationServices
namespace ApplicationServices;
public static class LibraryCommands
{
public static class LibraryCommands
public static event EventHandler<int>? ScanBegin;
public static event EventHandler<int>? ScanEnd;
public static bool Scanning { get; private set; }
/// <summary>
/// Serializes library scan and import operations so only one path reads/writes
/// <see cref="Book"/> / <see cref="LibraryBook"/> rows at a time (prevents duplicate ASIN inserts).
/// </summary>
private static readonly SemaphoreSlim ImportGate = new(1, 1);
static LibraryCommands()
{
public static event EventHandler<int> ScanBegin;
public static event EventHandler ScanEnd;
ScanBegin += (_, __) => Scanning = true;
ScanEnd += (_, __) => Scanning = false;
}
public static bool Scanning { get; private set; }
private static object _lock { get; } = new();
public static async Task<List<LibraryBook>> FindInactiveBooks(IEnumerable<LibraryBook> existingLibrary, params Account[] accounts)
{
logRestart();
static LibraryCommands()
if (accounts is null || accounts.Length == 0)
return new List<LibraryBook>();
await ImportGate.WaitAsync();
try
{
ScanBegin += (_, __) => Scanning = true;
ScanEnd += (_, __) => Scanning = false;
}
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, List<LibraryBook> existingLibrary, params Account[] accounts)
{
logRestart();
ScanBegin?.Invoke(null, accounts.Length);
//These are the minimum response groups required for the
//library scanner to pass all validation and filtering.
var libraryOptions = new LibraryOptions
{
ResponseGroups
{
ResponseGroups
= LibraryOptions.ResponseGroupOptions.ProductAttrs
| LibraryOptions.ResponseGroupOptions.ProductDesc
| LibraryOptions.ResponseGroupOptions.Relationships
};
if (accounts is null || accounts.Length == 0)
return new List<LibraryBook>();
try
{
logTime($"pre {nameof(scanAccountsAsync)} all");
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
var libraryItems = await scanAccountsAsync(accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = libraryItems.Count;
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
var missingBookList = existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList();
return missingBookList;
return existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList();
}
catch (AudibleApi.Authentication.LoginFailedException lfEx)
{
lfEx.SaveFiles(Configuration.Instance.LibationFiles);
lfEx.SaveFiles(Configuration.Instance.LibationFiles.Location);
// nuget Serilog.Exceptions would automatically log custom properties
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
@@ -83,33 +97,46 @@ namespace ApplicationServices
{
stop();
var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, 0);
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
}
}
#region FULL LIBRARY scan and import
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
finally
{
logRestart();
ImportGate.Release();
}
}
if (accounts is null || accounts.Length == 0)
return (0, 0);
#region FULL LIBRARY scan and import
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(params Account[]? accounts)
{
logRestart();
if (accounts is null || accounts.Length == 0)
return (0, 0);
await ImportGate.WaitAsync();
int newCount = 0;
try
{
ScanBegin?.Invoke(null, accounts.Length);
try
{
lock (_lock)
{
if (Scanning)
return (0, 0);
ScanBegin?.Invoke(null, accounts.Length);
}
logTime($"pre {nameof(scanAccountsAsync)} all");
var libraryOptions = new LibraryOptions
{
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS,
ResponseGroups
= LibraryOptions.ResponseGroupOptions.Rating | LibraryOptions.ResponseGroupOptions.Media
| LibraryOptions.ResponseGroupOptions.Relationships | LibraryOptions.ResponseGroupOptions.ProductDesc
| LibraryOptions.ResponseGroupOptions.Contributors | LibraryOptions.ResponseGroupOptions.ProvidedReview
| LibraryOptions.ResponseGroupOptions.ProductPlans | LibraryOptions.ResponseGroupOptions.Series
| LibraryOptions.ResponseGroupOptions.CategoryLadders | LibraryOptions.ResponseGroupOptions.ProductExtendedAttrs
| LibraryOptions.ResponseGroupOptions.PdfUrl | LibraryOptions.ResponseGroupOptions.OriginAsin
| LibraryOptions.ResponseGroupOptions.IsFinished,
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
};
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
var importItems = await scanAccountsAsync(accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = importItems.Count;
@@ -119,16 +146,16 @@ namespace ApplicationServices
return default;
Log.Logger.Information("Begin long-running import");
logTime($"pre {nameof(importIntoDbAsync)}");
var newCount = await importIntoDbAsync(importItems);
logTime($"post {nameof(importIntoDbAsync)}");
logTime($"pre {nameof(ImportIntoDbAsync)}");
newCount = await Task.Run(() => ImportIntoDbAsync(importItems));
logTime($"post {nameof(ImportIntoDbAsync)}");
Log.Logger.Information($"Import complete. New count {newCount}");
return (totalCount, newCount);
}
catch (AudibleApi.Authentication.LoginFailedException lfEx)
{
lfEx.SaveFiles(Configuration.Instance.LibationFiles);
lfEx.SaveFiles(Configuration.Instance.LibationFiles.Location);
// nuget Serilog.Exceptions would automatically log custom properties
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
@@ -153,202 +180,507 @@ namespace ApplicationServices
{
stop();
var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null);
ScanEnd?.Invoke(null, newCount);
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
}
}
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
finally
{
var tasks = new List<Task<List<ImportItem>>>();
foreach (var account in accounts)
{
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
var apiExtended = await apiExtendedfunc(account);
ImportGate.Release();
}
}
// add scanAccountAsync as a TASK: do not await
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions));
public static async Task<int> ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName)
{
await ImportGate.WaitAsync();
try
{
return importSingleToDb(item, accountId, localeName);
}
finally
{
ImportGate.Release();
}
}
private static int importSingleToDb(AudibleApi.Common.Item item, string accountId, string localeName)
{
ArgumentValidator.EnsureNotNull(item, nameof(item));
ArgumentValidator.EnsureNotNull(accountId, nameof(accountId));
ArgumentValidator.EnsureNotNull(localeName, nameof(localeName));
var importItem = new ImportItem(item, accountId, localeName);
var importItems = new List<ImportItem> { importItem };
var validator = new LibraryValidator();
var exceptions = validator.Validate(importItems.Select(i => i.DtoItem));
if (exceptions?.Any() ?? false)
{
Log.Logger.Error(new AggregateException(exceptions), "Error validating library book. {@DebugInfo}", new { item, accountId, localeName });
return 0;
}
return DoDbSizeChangeOperation(ctx =>
{
if (importItem.DtoItem.ProductId is null)
return;
var bookImporter = new BookImporter(ctx);
bookImporter.Import(importItems);
var book = ctx.LibraryBooks.FirstOrDefault(lb => lb.Book.AudibleProductId == importItem.DtoItem.ProductId);
if (book is null)
{
book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId);
ctx.LibraryBooks.Add(book);
}
// import library in parallel
var arrayOfLists = await Task.WhenAll(tasks);
var importItems = arrayOfLists.SelectMany(a => a).ToList();
return importItems;
}
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
else
{
Account = account?.MaskedLogEntry ?? "[null]"
});
book.AbsentFromLastScan = false;
}
book.SetIncludedUntil(importItem.DtoItem.GetExpirationDate());
book.SetIsAudiblePlus(importItem.DtoItem.IsAyce is true);
});
}
logTime($"pre scanAccountAsync {account.AccountName}");
private static LogArchiver? openLogArchive(string? archivePath)
{
if (string.IsNullOrWhiteSpace(archivePath))
return null;
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
try
{
return new LogArchiver(archivePath);
}
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
{
logTime("importIntoDbAsync -- pre db");
using var context = DbContexts.GetContext();
var libraryBookImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()");
int qtyChanges = saveChanges(context);
logTime("importIntoDbAsync -- post SaveChanges");
// this is any changes at all to the database, not just new books
if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange());
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
return newCount;
}
private static int saveChanges(LibationContext context)
catch (System.IO.InvalidDataException)
{
try
{
return context.SaveChanges();
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
{
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culpret is the "WithExceptionDetails" serilog extension
static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}";
var msg = "Microsoft.EntityFrameworkCore.DbUpdateException";
if (ex.InnerException is null)
throw new Exception($"{msg}{format(ex)}");
throw new Exception(
$"{msg}{format(ex)}",
new Exception($"Inner Exception{format(ex.InnerException)}"));
}
}
#endregion
#region remove books
public static Task<List<LibraryBook>> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
private static List<LibraryBook> removeBooks(List<string> idsToRemove)
{
using var context = DbContexts.GetContext();
var libBooks = context.GetLibrary_Flat_NoTracking();
var removeLibraryBooks = libBooks.Where(lb => idsToRemove.Contains(lb.Book.AudibleProductId)).ToList();
context.LibraryBooks.RemoveRange(removeLibraryBooks);
context.Books.RemoveRange(removeLibraryBooks.Select(lb => lb.Book));
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
finalizeLibrarySizeChange();
return removeLibraryBooks;
}
#endregion
// call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange()
{
SearchEngineCommands.FullReIndex();
LibrarySizeChanged?.Invoke(null, null);
}
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
public static event EventHandler LibrarySizeChanged;
/// <summary>
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
/// </summary>
public static event EventHandler BookUserDefinedItemCommitted;
#region Update book details
public static int UpdateUserDefinedItem(params Book[] books) => UpdateUserDefinedItem(books.ToList());
public static int UpdateUserDefinedItem(IEnumerable<Book> books)
{
try
{
if (books is null || !books.Any())
return 0;
using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges()
foreach (var book in books)
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges == 0)
return 0;
// semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
// I did not benchmark before choosing the number here
if (qtyChanges > 15)
SearchEngineCommands.FullReIndex();
else
{
foreach (var book in books)
{
SearchEngineCommands.UpdateLiberatedStatus(book);
SearchEngineCommands.UpdateBookTags(book);
}
}
BookUserDefinedItemCommitted?.Invoke(null, null);
return qtyChanges;
Log.Logger.Warning($"Deleting corrupted {nameof(LogArchiver)} at {archivePath}");
FileUtility.SaferDelete(archivePath);
return new LogArchiver(archivePath);
}
catch (Exception ex)
{
Log.Logger.Error(ex, $"Error updating {nameof(Book.UserDefinedItem)}");
throw;
Log.Logger.Error(ex, $"Failed to open {nameof(LogArchiver)} at {archivePath}");
}
}
#endregion
// must be here instead of in db layer due to AaxcExists
public static LiberatedStatus Liberated_Status(Book book)
=> book.Audio_Exists() ? book.UserDefinedItem.BookStatus
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
: LiberatedStatus.NotLiberated;
// exists here for feature predictability. It makes sense for this to be where Liberated_Status is
public static LiberatedStatus? Pdf_Status(Book book) => book.UserDefinedItem.PdfStatus;
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded) { }
public static LibraryStats GetCounts()
catch (Exception ex)
{
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
Log.Logger.Error(ex, $"Failed to open {nameof(LogArchiver)} at {archivePath}");
}
return null;
}
var results = libraryBooks
.AsParallel()
.Select(lb => Liberated_Status(lb.Book))
.ToList();
var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
var booksError = results.Count(r => r == LiberatedStatus.Error);
private static async Task<List<ImportItem>> scanAccountsAsync(Account[] accounts, LibraryOptions libraryOptions)
{
var tasks = new List<Task<List<ImportItem>>>();
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError });
await using LogArchiver? archiver
= Log.Logger.IsDebugEnabled()
? openLogArchive(System.IO.Path.Combine(Configuration.Instance.LibationFiles.Location, "LibraryScans.zip"))
: default;
var boolResults = libraryBooks
.AsParallel()
.Where(lb => lb.Book.HasPdf())
.Select(lb => Pdf_Status(lb.Book))
.ToList();
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated);
archiver?.DeleteAllButNewestN(20);
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });
foreach (var account in accounts)
{
try
{
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
var apiExtended = await ApiExtended.CreateAsync(account);
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, pdfsDownloaded, pdfsNotDownloaded);
// add scanAccountAsync as a TASK: do not await
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions, archiver));
}
catch (Exception ex)
{
//Catch to allow other accounts to continue scanning.
Log.Logger.Error(ex, "Failed to scan account");
}
}
// import library in parallel
var arrayOfLists = await Task.WhenAll(tasks);
var importItems = arrayOfLists.SelectMany(a => a).ToList();
return importItems;
}
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver? archiver)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
var locale = ArgumentValidator.EnsureNotNull(account.Locale, nameof(account.Locale));
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
{
Account = account.MaskedLogEntry ?? "[null]"
});
logTime($"pre scanAccountAsync {account.AccountName}");
try
{
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions);
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
await logDtoItemsAsync(dtoItems);
return dtoItems.Select(d => new ImportItem(d, account.AccountId, locale.Name)).ToList();
}
catch (ImportValidationException ex)
{
await logDtoItemsAsync(ex.Items, ex.InnerExceptions.ToArray());
//If ImportValidationException is thrown, all Dto items get logged as part of the exception
throw new AggregateException(ex.InnerExceptions);
}
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception>? exceptions = null)
{
if (archiver is not null)
{
var fileName = $"{DateTime.Now:u} {account.MaskedLogEntry}.json";
var items = await Task.Run(() => JArray.FromObject(dtoItems.Select(i => i.SourceJson)));
var scanFile = new JObject
{
{ "Account", account.MaskedLogEntry },
{ "ScannedDateTime", DateTime.Now.ToString("u") },
};
if (exceptions?.Any() is true)
scanFile.Add("Exceptions", JArray.FromObject(exceptions));
scanFile.Add("Items", items);
await archiver.AddFileAsync(fileName, scanFile);
}
}
}
private static async Task<int> ImportIntoDbAsync(List<ImportItem> importItems) => await Task.Run(() => importIntoDb(importItems));
private static int importIntoDb(List<ImportItem> importItems)
{
logTime("importIntoDbAsync -- pre db");
int newCount = 0;
DoDbSizeChangeOperation(ctx =>
{
var libraryBookImporter = new LibraryBookImporter(ctx);
newCount = libraryBookImporter.Import(importItems);
logTime("importIntoDbAsync -- post Import()");
});
return newCount;
}
public static int SaveContext(LibationContext context)
{
try
{
return context.SaveChanges();
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
{
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culprit is the "WithExceptionDetails" serilog extension
static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}";
var msg = "Microsoft.EntityFrameworkCore.DbUpdateException";
if (ex.InnerException is null)
throw new Exception($"{msg}{format(ex)}");
throw new Exception(
$"{msg}{format(ex)}",
new Exception($"Inner Exception{format(ex.InnerException)}"));
}
}
#endregion
#region remove/restore books
public static Task<int> RemoveBooksAsync(this IEnumerable<LibraryBook?>? idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
private static int removeBooks(IEnumerable<LibraryBook?>? removeLibraryBooks)
{
if (removeLibraryBooks is null || !removeLibraryBooks.Any())
return 0;
return DoDbSizeChangeOperation(ctx =>
{
// Entry() NoTracking entities before SaveChanges()
foreach (var lb in removeLibraryBooks.OfType<LibraryBook>())
{
lb.IsDeleted = true;
ctx.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
});
}
public static Task<int> RestoreBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => restoreBooks(idsToRemove));
private static int restoreBooks(this IEnumerable<LibraryBook> libraryBooks)
{
if (libraryBooks is null || !libraryBooks.Any())
return 0;
try
{
return DoDbSizeChangeOperation(ctx =>
{
// Entry() NoTracking entities before SaveChanges()
foreach (var lb in libraryBooks)
{
lb.IsDeleted = false;
ctx.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
});
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error restoring books");
throw;
}
}
public static Task<int> PermanentlyDeleteBooksAsync(this IEnumerable<LibraryBook?>? idsToRemove) => Task.Run(() => permanentlyDeleteBooks(idsToRemove));
private static int permanentlyDeleteBooks(this IEnumerable<LibraryBook?>? libraryBooks)
{
if (libraryBooks is null || !libraryBooks.Any())
return 0;
try
{
return DoDbSizeChangeOperation(ctx =>
{
ctx.LibraryBooks.RemoveRange(libraryBooks.OfType<LibraryBook>());
ctx.Books.RemoveRange(libraryBooks.OfType<LibraryBook>().Select(lb => lb.Book));
});
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error restoring books");
throw;
}
}
static int DoDbSizeChangeOperation(Action<LibationContext> action)
{
try
{
int qtyChanges;
List<LibraryBook>? library;
using (var context = DbContexts.GetContext())
{
action?.Invoke(context);
qtyChanges = SaveContext(context);
logTime("importIntoDbAsync -- post SaveChanges");
library = qtyChanges == 0 ? null : context.GetLibrary_Flat_NoTracking(includeParents: true);
}
if (library is not null)
finalizeLibrarySizeChange(library);
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error performing DB Size change operation");
throw;
}
}
#endregion
// call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange(List<LibraryBook> library)
{
LibrarySizeChanged?.Invoke(null, library);
}
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
public static event EventHandler<List<LibraryBook>>? LibrarySizeChanged;
/// <summary>
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
/// </summary>
public static event EventHandler<IEnumerable<LibraryBook>>? BookUserDefinedItemCommitted;
#region Update book details
public static async Task<int> UpdateUserDefinedItemAsync(
this LibraryBook lb,
string? tags = null,
LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null,
Rating? rating = null)
=> await UpdateUserDefinedItemAsync([lb], tags, bookStatus, pdfStatus, rating);
public static async Task<int> UpdateUserDefinedItemAsync(
this IEnumerable<LibraryBook> lb,
string? tags = null,
LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null,
Rating? rating = null)
=> await UpdateUserDefinedItemAsync(
lb,
udi =>
{
// blank tags are expected. null tags are not
if (tags is not null)
udi.Tags = tags;
if (bookStatus.HasValue)
udi.BookStatus = bookStatus.Value;
// method handles null logic
udi.SetPdfStatus(pdfStatus);
if (rating is not null)
udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating);
});
public static async Task<int> UpdateBookStatusAsync(this LibraryBook lb, LiberatedStatus bookStatus, Version? libationVersion, AudioFormat? audioFormat, string audioVersion)
=> await lb.UpdateUserDefinedItemAsync(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion, audioFormat, audioVersion); });
public static async Task<int> UpdateBookStatusAsync(this LibraryBook libraryBook, LiberatedStatus bookStatus)
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.BookStatus = bookStatus);
public static async Task<int> UpdateBookStatusAsync(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus bookStatus)
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.BookStatus = bookStatus);
public static async Task<int> UpdatePdfStatusAsync(this LibraryBook libraryBook, LiberatedStatus pdfStatus)
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.SetPdfStatus(pdfStatus));
public static async Task<int> UpdatePdfStatusAsync(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus pdfStatus)
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.SetPdfStatus(pdfStatus));
public static async Task<int> UpdateTagsAsync(this LibraryBook libraryBook, string tags)
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.Tags = tags);
public static async Task<int> UpdateTagsAsync(this IEnumerable<LibraryBook> libraryBooks, string? tags)
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.Tags = tags ?? string.Empty);
public static async Task<int> UpdateUserDefinedItemAsync(this LibraryBook libraryBook, Action<UserDefinedItem> action)
=> await UpdateUserDefinedItemAsync([libraryBook], action);
public static Task<int> UpdateUserDefinedItemAsync(this IEnumerable<LibraryBook?>? libraryBooks, Action<UserDefinedItem> action)
=> Task.Run(() => libraryBooks.updateUserDefinedItem(action));
private static int updateUserDefinedItem(this IEnumerable<LibraryBook?>? libraryBooks, Action<UserDefinedItem> action)
{
try
{
if (libraryBooks is null || !libraryBooks.Any())
return 0;
var nonNullBooks = libraryBooks.OfType<LibraryBook>();
if (!nonNullBooks.Any())
return 0;
int qtyChanges;
using (var context = DbContexts.GetContext())
{
// Entry() instead of Attach() due to possible stack overflow with large tables
foreach (var book in nonNullBooks)
{
action?.Invoke(book.Book.UserDefinedItem);
var udiEntity = context.Entry(book.Book.UserDefinedItem);
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Rating> ratingEntry)
ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
qtyChanges = context.SaveChanges();
}
if (qtyChanges > 0)
BookUserDefinedItemCommitted?.Invoke(null, nonNullBooks);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, $"Error updating {nameof(Book.UserDefinedItem)}");
throw;
}
}
#endregion
// must be here instead of in db layer due to AaxcExists
public static LiberatedStatus Liberated_Status(Book book)
=> book.AudioExists ? book.UserDefinedItem.BookStatus
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
: LiberatedStatus.NotLiberated;
// exists here for feature predictability. It makes sense for this to be where Liberated_Status is
public static LiberatedStatus? Pdf_Status(Book book) => book.UserDefinedItem.PdfStatus;
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int booksUnavailable, int pdfsDownloaded, int pdfsNotDownloaded, int pdfsUnavailable, IEnumerable<LibraryBook> LibraryBooks)
{
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
public bool HasPendingBooks => PendingBooks > 0;
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError + booksUnavailable);
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded + pdfsUnavailable);
public string StatusString => HasPdfResults ? $"{toBookStatusString()} | {toPdfStatusString()}" : toBookStatusString();
private string toBookStatusString()
{
if (!HasBookResults) return "No books. Begin by importing your library";
if (!HasPendingBooks && booksError + booksUnavailable == 0) return $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up";
var sb = new StringBuilder($"BACKUPS: No progress: {booksNoProgress} In process: {booksDownloadedOnly} Fully backed up: {booksFullyBackedUp}");
if (booksError > 0)
sb.Append($" Errors: {booksError}");
if (booksUnavailable > 0)
sb.Append($" Unavailable: {booksUnavailable}");
return sb.ToString();
}
private string toPdfStatusString()
{
if (pdfsNotDownloaded + pdfsUnavailable == 0) return $"All {pdfsDownloaded} PDFs downloaded";
var sb = new StringBuilder($"PDFs: NOT d/l'ed: {pdfsNotDownloaded} Downloaded: {pdfsDownloaded}");
if (pdfsUnavailable > 0)
sb.Append($" Unavailable: {pdfsUnavailable}");
return sb.ToString();
}
}
public static LibraryStats GetCounts(IEnumerable<LibraryBook>? libraryBooks = null)
{
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
var results = libraryBooks
.AsParallel()
.WithoutParents()
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
.ToList();
var booksFullyBackedUp = results.Count(r => r.status == LiberatedStatus.Liberated);
var booksDownloadedOnly = results.Count(r => !r.absent && r.status == LiberatedStatus.PartialDownload);
var booksNoProgress = results.Count(r => !r.absent && r.status == LiberatedStatus.NotLiberated);
var booksError = results.Count(r => r.status == LiberatedStatus.Error);
var booksUnavailable = results.Count(r => r.absent && r.status is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload);
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable });
var pdfResults = libraryBooks
.AsParallel()
.Where(lb => lb.Book.HasPdf)
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Pdf_Status(lb.Book) })
.ToList();
var pdfsDownloaded = pdfResults.Count(r => r.status == LiberatedStatus.Liberated);
var pdfsNotDownloaded = pdfResults.Count(r => !r.absent && r.status == LiberatedStatus.NotLiberated);
var pdfsUnavailable = pdfResults.Count(r => r.absent && r.status == LiberatedStatus.NotLiberated);
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = pdfResults.Count, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable });
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable, libraryBooks);
}
}
+60 -266
View File
@@ -1,287 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ClosedXML.Excel;
using CsvHelper;
using CsvHelper.Configuration.Attributes;
using DataLayer;
using NPOI.XSSF.UserModel;
using Serilog;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
namespace ApplicationServices
namespace ApplicationServices;
public static class LibraryExporter
{
public class ExportDto
public static void ToCsv(string saveFilePath, IEnumerable<LibraryBook>? libraryBooks = null)
{
public static string GetName(string fieldName)
{
var property = typeof(ExportDto).GetProperty(fieldName);
var attribute = property.GetCustomAttributes(typeof(NameAttribute), true)[0];
var description = (NameAttribute)attribute;
var text = description.Names;
return text[0];
}
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
var dtos = libraryBooks.ToDtos();
if (dtos.Count == 0)
return;
[Name("Account")]
public string Account { get; set; }
[Name("Date Added to library")]
public DateTime DateAdded { get; set; }
[Name("Audible Product Id")]
public string AudibleProductId { get; set; }
[Name("Locale")]
public string Locale { get; set; }
[Name("Title")]
public string Title { get; set; }
[Name("Authors")]
public string AuthorNames { get; set; }
[Name("Narrators")]
public string NarratorNames { get; set; }
[Name("Length In Minutes")]
public int LengthInMinutes { get; set; }
[Name("Description")]
public string Description { get; set; }
[Name("Publisher")]
public string Publisher { get; set; }
[Name("Has PDF")]
public bool HasPdf { get; set; }
[Name("Series Names")]
public string SeriesNames { get; set; }
[Name("Series Order")]
public string SeriesOrder { get; set; }
[Name("Community Rating: Overall")]
public float? CommunityRatingOverall { get; set; }
[Name("Community Rating: Performance")]
public float? CommunityRatingPerformance { get; set; }
[Name("Community Rating: Story")]
public float? CommunityRatingStory { get; set; }
[Name("Cover Id")]
public string PictureId { get; set; }
[Name("Is Abridged?")]
public bool IsAbridged { get; set; }
[Name("Date Published")]
public DateTime? DatePublished { get; set; }
[Name("Categories")]
public string CategoriesNames { get; set; }
[Name("My Rating: Overall")]
public float? MyRatingOverall { get; set; }
[Name("My Rating: Performance")]
public float? MyRatingPerformance { get; set; }
[Name("My Rating: Story")]
public float? MyRatingStory { get; set; }
[Name("My Libation Tags")]
public string MyLibationTags { get; set; }
[Name("Book Liberated Status")]
public string BookStatus { get; set; }
[Name("PDF Liberated Status")]
public string PdfStatus { get; set; }
[Name("Content Type")]
public string ContentType { get; set; }
using var csv = new CsvWriter(new System.IO.StreamWriter(saveFilePath), CultureInfo.CurrentCulture);
csv.WriteHeader(typeof(ExportDto));
csv.NextRecord();
csv.WriteRecords(dtos);
}
public static class LibToDtos
public static void ToJson(string saveFilePath, IEnumerable<LibraryBook>? libraryBooks = null)
{
public static List<ExportDto> ToDtos(this IEnumerable<LibraryBook> library)
=> library.Select(a => new ExportDto
{
Account = a.Account,
DateAdded = a.DateAdded,
AudibleProductId = a.Book.AudibleProductId,
Locale = a.Book.Locale,
Title = a.Book.Title,
AuthorNames = a.Book.AuthorNames(),
NarratorNames = a.Book.NarratorNames(),
LengthInMinutes = a.Book.LengthInMinutes,
Description = a.Book.Description,
Publisher = a.Book.Publisher,
HasPdf = a.Book.HasPdf(),
SeriesNames = a.Book.SeriesNames(),
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
CommunityRatingOverall = a.Book.Rating?.OverallRating,
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating,
CommunityRatingStory = a.Book.Rating?.StoryRating,
PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.CategoriesNames().Any() ? a.Book.CategoriesNames().Aggregate((a, b) => $"{a}, {b}") : "",
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
MyLibationTags = a.Book.UserDefinedItem.Tags,
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
ContentType = a.Book.ContentType.ToString()
}).ToList();
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
var dtos = libraryBooks.ToDtos();
var serializer = new JsonSerializer();
using var writer = new JsonTextWriter(new System.IO.StreamWriter(saveFilePath)) { Formatting = Formatting.Indented };
serializer.Serialize(writer, dtos);
}
public static class LibraryExporter
public static void ToXlsx(string saveFilePath, IEnumerable<LibraryBook>? libraryBooks = null)
{
public static void ToCsv(string saveFilePath)
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
var dtos = libraryBooks.ToDtos();
using var workbook = new XLWorkbook();
var sheet = workbook.AddWorksheet("Library");
var columns = typeof(ExportDto).GetProperties().Where(p => p.GetCustomAttribute<NameAttribute>() is not null).ToArray();
// headers
var currentRow = sheet.FirstRow();
var currentCell = currentRow.FirstCell();
foreach (var column in columns)
{
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
if (!dtos.Any())
return;
using var writer = new System.IO.StreamWriter(saveFilePath);
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
csv.WriteHeader(typeof(ExportDto));
csv.NextRecord();
csv.WriteRecords(dtos);
currentCell.Value = GetColumnName(column);
currentCell.Style.Font.Bold = true;
currentCell = currentCell.CellRight();
}
public static void ToJson(string saveFilePath)
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
// Add data rows
foreach (var dto in dtos)
{
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
var json = Newtonsoft.Json.JsonConvert.SerializeObject(dtos, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(saveFilePath, json);
}
currentRow = currentRow.RowBelow();
currentCell = currentRow.FirstCell();
public static void ToXlsx(string saveFilePath)
{
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Library");
var detailSubtotalFont = workbook.CreateFont();
detailSubtotalFont.IsBold = true;
var detailSubtotalCellStyle = workbook.CreateCellStyle();
detailSubtotalCellStyle.SetFont(detailSubtotalFont);
// headers
var rowIndex = 0;
var row = sheet.CreateRow(rowIndex);
var columns = new[] {
nameof (ExportDto.Account),
nameof (ExportDto.DateAdded),
nameof (ExportDto.AudibleProductId),
nameof (ExportDto.Locale),
nameof (ExportDto.Title),
nameof (ExportDto.AuthorNames),
nameof (ExportDto.NarratorNames),
nameof (ExportDto.LengthInMinutes),
nameof (ExportDto.Description),
nameof (ExportDto.Publisher),
nameof (ExportDto.HasPdf),
nameof (ExportDto.SeriesNames),
nameof (ExportDto.SeriesOrder),
nameof (ExportDto.CommunityRatingOverall),
nameof (ExportDto.CommunityRatingPerformance),
nameof (ExportDto.CommunityRatingStory),
nameof (ExportDto.PictureId),
nameof (ExportDto.IsAbridged),
nameof (ExportDto.DatePublished),
nameof (ExportDto.CategoriesNames),
nameof (ExportDto.MyRatingOverall),
nameof (ExportDto.MyRatingPerformance),
nameof (ExportDto.MyRatingStory),
nameof (ExportDto.MyLibationTags),
nameof (ExportDto.BookStatus),
nameof (ExportDto.PdfStatus),
nameof (ExportDto.ContentType)
};
var col = 0;
foreach (var c in columns)
foreach (var column in columns)
{
var cell = row.CreateCell(col++);
var name = ExportDto.GetName(c);
cell.SetCellValue(name);
cell.CellStyle = detailSubtotalCellStyle;
var value = column.GetValue(dto);
currentCell.Value = XLCellValue.FromObject(value);
currentCell.Style.DateFormat.Format = currentCell.DataType is XLDataType.DateTime ? dateFormat : string.Empty;
currentCell = currentCell.CellRight();
}
var dateFormat = workbook.CreateDataFormat();
var dateStyle = workbook.CreateCellStyle();
dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
rowIndex++;
// Add data rows
foreach (var dto in dtos)
{
col = 0;
row = sheet.CreateRow(rowIndex);
row.CreateCell(col++).SetCellValue(dto.Account);
var dateAddedCell = row.CreateCell(col++);
dateAddedCell.CellStyle = dateStyle;
dateAddedCell.SetCellValue(dto.DateAdded);
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
row.CreateCell(col++).SetCellValue(dto.Locale);
row.CreateCell(col++).SetCellValue(dto.Title);
row.CreateCell(col++).SetCellValue(dto.AuthorNames);
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
row.CreateCell(col++).SetCellValue(dto.Description);
row.CreateCell(col++).SetCellValue(dto.Publisher);
row.CreateCell(col++).SetCellValue(dto.HasPdf);
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
col = createCell(row, col, dto.CommunityRatingOverall);
col = createCell(row, col, dto.CommunityRatingPerformance);
col = createCell(row, col, dto.CommunityRatingStory);
row.CreateCell(col++).SetCellValue(dto.PictureId);
row.CreateCell(col++).SetCellValue(dto.IsAbridged);
var datePubCell = row.CreateCell(col++);
datePubCell.CellStyle = dateStyle;
if (dto.DatePublished.HasValue)
datePubCell.SetCellValue(dto.DatePublished.Value);
else
datePubCell.SetCellValue("");
row.CreateCell(col++).SetCellValue(dto.CategoriesNames);
col = createCell(row, col, dto.MyRatingOverall);
col = createCell(row, col, dto.MyRatingPerformance);
col = createCell(row, col, dto.MyRatingStory);
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
row.CreateCell(col++).SetCellValue(dto.BookStatus);
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
row.CreateCell(col++).SetCellValue(dto.ContentType);
rowIndex++;
}
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
workbook.Write(fileData);
}
private static int createCell(NPOI.SS.UserModel.IRow row, int col, float? nullableFloat)
{
if (nullableFloat.HasValue)
row.CreateCell(col++).SetCellValue(nullableFloat.Value);
else
row.CreateCell(col++).SetCellValue("");
return col;
}
workbook.SaveAs(saveFilePath);
}
private static List<ExportDto> ToDtos(this IEnumerable<LibraryBook> library)
=> library.Select(a => new ExportDto(a)).ToList();
private static string GetColumnName(PropertyInfo property)
=> property.GetCustomAttribute<NameAttribute>()?.Names?.FirstOrDefault() ?? property.Name;
}
@@ -0,0 +1,15 @@
using LibationSearchEngine;
namespace ApplicationServices;
/// <summary>
/// The main search engine used Libation.
/// Acts as an adapter to SearchEngineCommands.Search()
/// </summary>
public class MainSearchEngine : ISearchEngine
{
public static MainSearchEngine Instance { get; } = new MainSearchEngine();
private MainSearchEngine() { }
public SearchResultSet? GetSearchResultSet(string? searchString)
=> string.IsNullOrEmpty(searchString) ? null : SearchEngineCommands.Search(searchString);
}
@@ -0,0 +1,185 @@
using AudibleApi.Common;
using ClosedXML.Excel;
using CsvHelper;
using DataLayer;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace ApplicationServices;
public static class RecordExporter
{
public static void ToXlsx(string saveFilePath, IEnumerable<IRecord> records)
{
if (!records.Any())
return;
using var workbook = new XLWorkbook();
var worksheet = workbook.AddWorksheet("Records");
// headers
var columns = new List<string>
{
nameof(Type.Name),
nameof(IRecord.Created),
nameof(IRecord.Start) + "_ms",
};
if (records.OfType<IAnnotation>().Any())
{
columns.Add(nameof(IAnnotation.AnnotationId));
columns.Add(nameof(IAnnotation.LastModified));
}
if (records.OfType<IRangeAnnotation>().Any())
{
columns.Add(nameof(IRangeAnnotation.End) + "_ms");
columns.Add(nameof(IRangeAnnotation.Text));
}
if (records.OfType<Clip>().Any())
columns.Add(nameof(Clip.Title));
int rowIndex = 1, col = 1;
var headerRow = worksheet.Row(rowIndex++);
foreach (var c in columns)
{
var headerCell = headerRow.Cell(col++);
headerCell.Value = c;
headerCell.Style.Font.Bold = true;
}
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
// Add data rows
foreach (var record in records)
{
col = 1;
var row = worksheet.Row(rowIndex++);
row.Cell(col++).Value = record.GetType().Name;
row.Cell(col++).SetDate(record.Created.DateTime, dateFormat);
row.Cell(col++).Value = record.Start.TotalMilliseconds;
if (record is IAnnotation annotation)
{
row.Cell(col++).Value = annotation.AnnotationId;
row.Cell(col++).SetDate(annotation.LastModified.DateTime, dateFormat);
if (annotation is IRangeAnnotation rangeAnnotation)
{
row.Cell(col++).Value = rangeAnnotation.End.TotalMilliseconds;
row.Cell(col++).Value = rangeAnnotation.Text;
if (rangeAnnotation is Clip clip)
row.Cell(col++).Value = clip.Title;
}
}
}
workbook.SaveAs(saveFilePath);
}
private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat)
{
cell.Value = value;
cell.Style.DateFormat.Format = dateFormat;
}
public static void ToJson(string saveFilePath, LibraryBook libraryBook, IEnumerable<IRecord> records)
{
if (!records.Any())
return;
var recordsEx = extendRecords(records);
var recordsObj = new JObject
{
{ "title", libraryBook.Book.TitleWithSubtitle},
{ "asin", libraryBook.Book.AudibleProductId},
{ "exportTime", DateTime.Now},
{ "records", JArray.FromObject(recordsEx) }
};
System.IO.File.WriteAllText(saveFilePath, recordsObj.ToString(Newtonsoft.Json.Formatting.Indented));
}
public static void ToCsv(string saveFilePath, IEnumerable<IRecord> records)
{
if (!records.Any())
return;
using var writer = new System.IO.StreamWriter(saveFilePath);
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
//Write headers for the present record type that has the most properties
if (records.OfType<Clip>().Any())
csv.WriteHeader(typeof(ClipEx));
else if (records.OfType<Note>().Any())
csv.WriteHeader(typeof(NoteEx));
else if (records.OfType<Bookmark>().Any())
csv.WriteHeader(typeof(BookmarkEx));
else
csv.WriteHeader(typeof(LastHeardEx));
var recordsEx = extendRecords(records);
csv.NextRecord();
csv.WriteRecords(recordsEx.OfType<ClipEx>());
csv.WriteRecords(recordsEx.OfType<NoteEx>());
csv.WriteRecords(recordsEx.OfType<BookmarkEx>());
csv.WriteRecords(recordsEx.OfType<LastHeardEx>());
}
private static IEnumerable<IRecordEx> extendRecords(IEnumerable<IRecord> records)
=> records
.Select<IRecord, IRecordEx>(
r => r switch
{
Clip c => new ClipEx(nameof(Clip), c),
Note n => new NoteEx(nameof(Note), n),
Bookmark b => new BookmarkEx(nameof(Bookmark), b),
LastHeard l => new LastHeardEx(nameof(LastHeard), l),
_ => throw new InvalidOperationException(),
});
private interface IRecordEx { string Type { get; } }
private record LastHeardEx : LastHeard, IRecordEx
{
public string Type { get; }
public LastHeardEx(string type, LastHeard original) : base(original)
{
Type = type;
}
}
private record BookmarkEx : Bookmark, IRecordEx
{
public string Type { get; }
public BookmarkEx(string type, Bookmark original) : base(original)
{
Type = type;
}
}
private record NoteEx : Note, IRecordEx
{
public string Type { get; }
public NoteEx(string type, Note original) : base(original)
{
Type = type;
}
}
private record ClipEx : Clip, IRecordEx
{
public string Type { get; }
public ClipEx(string type, Clip original) : base(original)
{
Type = type;
}
}
}
@@ -1,46 +1,26 @@
using System;
using System.IO;
using DataLayer;
using DataLayer;
using LibationSearchEngine;
using Serilog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace ApplicationServices
namespace ApplicationServices;
public static class SearchEngineCommands
{
public static class SearchEngineCommands
/// <summary>Serializes all search index access so only one reader/writer is active at a time, avoiding write.lock contention.</summary>
private static readonly object IndexLock = new();
#region Search
public static SearchResultSet Search(string searchString) => performSafeQuery(e =>
e.Search(searchString)
);
private static T performSafeQuery<T>(Func<SearchEngine, T> func)
{
public static void FullReIndex(SearchEngine engine = null)
{
engine ??= new SearchEngine();
var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library);
}
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
e.Search(searchString)
);
public static void UpdateBookTags(Book book) => performSearchEngineAction_safe(e =>
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
);
public static void UpdateLiberatedStatus(Book book) => performSearchEngineAction_safe(e =>
e.UpdateLiberatedStatus(book)
);
private static void performSearchEngineAction_safe(Action<SearchEngine> action)
{
var engine = new SearchEngine();
try
{
action(engine);
}
catch (FileNotFoundException)
{
FullReIndex(engine);
action(engine);
}
}
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> func)
lock (IndexLock)
{
var engine = new SearchEngine();
try
@@ -49,9 +29,99 @@ namespace ApplicationServices
}
catch (FileNotFoundException)
{
FullReIndex(engine);
fullReIndex(engine);
return func(engine);
}
catch (Exception ex) when (SearchEngine.IsRecoverableCorruptIndexException(ex))
{
Log.Warning(ex, "Search index unreadable or corrupt; rebuilding and retrying query.");
fullReIndex(engine);
return func(engine);
}
}
}
#endregion
public static event EventHandler? SearchEngineUpdated;
#region Update
private static bool isUpdating;
public static void UpdateBooks(IEnumerable<LibraryBook> books)
{
// Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
// I did not benchmark before choosing the number here
if (books.Count() > 15)
FullReIndex();
else
{
foreach (var book in books)
UpdateUserDefinedItems(book);
}
}
public static void FullReIndex() => performSafeCommand(fullReIndex);
public static void FullReIndex(List<LibraryBook> libraryBooks)
=> performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents()));
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
{
e.UpdateLiberatedStatus(book);
e.UpdateTags(book.Book.AudibleProductId, book.Book.UserDefinedItem.Tags);
e.UpdateUserRatings(book);
}
);
private static void performSafeCommand(Action<SearchEngine> action)
{
try
{
update(action);
}
catch (FileNotFoundException)
{
fullReIndex(new SearchEngine());
update(action);
}
catch (Exception ex) when (SearchEngine.IsRecoverableCorruptIndexException(ex))
{
Log.Warning(ex, "Search index unreadable or corrupt; rebuilding and retrying.");
fullReIndex(new SearchEngine());
update(action);
}
}
private static void update(Action<SearchEngine> action)
{
if (action is null)
return;
lock (IndexLock)
{
// support nesting incl recursion
var prevIsUpdating = isUpdating;
try
{
isUpdating = true;
action(new SearchEngine());
if (!prevIsUpdating)
SearchEngineUpdated?.Invoke(null, EventArgs.Empty);
}
finally
{
isUpdating = prevIsUpdating;
}
}
}
private static void fullReIndex(SearchEngine engine)
{
var library = DbContexts.GetLibrary_Flat_NoTracking();
fullReIndex(engine, library);
}
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
=> engine.CreateNewIndex(libraryBooks);
#endregion
}
@@ -0,0 +1,44 @@
using DataLayer;
using LibationFileManager;
using LibationSearchEngine;
using System.Collections.Generic;
namespace ApplicationServices;
/// <summary>
/// A temporary search engine created in InProgress/TempSearchEngine
/// Used for Trash Bin searches to avoid interfering with the main search engine
/// </summary>
public class TempSearchEngine : ISearchEngine
{
public static string SearchEnginePath { get; }
= System.IO.Path.Combine(Configuration.Instance.InProgress, nameof(TempSearchEngine));
private SearchEngine SearchEngine { get; } = new SearchEngine(SearchEnginePath);
public bool ReindexSearchEngine(IEnumerable<LibraryBook> books)
{
try
{
SearchEngine.CreateNewIndex(books, overwrite: true);
return true;
}
catch
{
return false;
}
}
public SearchResultSet? GetSearchResultSet(string? searchString)
{
if (string.IsNullOrEmpty(searchString))
return null;
try
{
return SearchEngine.Search(searchString);
}
catch
{
return null;
}
}
}
+88 -93
View File
@@ -1,103 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApi;
using AudibleApi;
using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
using System;
using System.Diagnostics.CodeAnalysis;
namespace AudibleUtilities
namespace AudibleUtilities;
public class Account : IUpdatable
{
public class Account : IUpdatable
public event EventHandler? Updated;
private void update(object? sender = null, EventArgs? e = null)
=> Updated?.Invoke(this, EventArgs.Empty);
// canonical. immutable. email or phone number
public string AccountId { get; }
// user-friendly, non-canonical name. mutable
public string? AccountName
{
public event EventHandler Updated;
private void update(object sender = null, EventArgs e = null)
=> Updated?.Invoke(this, new EventArgs());
// canonical. immutable. email or phone number
public string AccountId { get; }
// user-friendly, non-canonical name. mutable
private string _accountName;
public string AccountName
get => field;
set
{
get => _accountName;
set
{
if (string.IsNullOrWhiteSpace(value))
return;
var v = value.Trim();
if (v == _accountName)
return;
_accountName = v;
update();
}
if (string.IsNullOrWhiteSpace(value))
return;
var v = value.Trim();
if (v == field)
return;
field = v;
update();
}
// whether to include this account when scanning libraries.
// technically this is an app setting; not an attribute of account. but since it's managed with accounts, it makes sense to put this exception-to-the-rule here
private bool _libraryScan = true;
public bool LibraryScan
{
get => _libraryScan;
set
{
if (value == _libraryScan)
return;
_libraryScan = value;
update();
}
}
private string _decryptKey = "";
/// <summary>aka: activation bytes</summary>
public string DecryptKey
{
get => _decryptKey;
set
{
var v = (value ?? "").Trim();
if (v == _decryptKey)
return;
_decryptKey = v;
update();
}
}
private Identity _identity;
public Identity IdentityTokens
{
get => _identity;
set
{
if (_identity is null && value is null)
return;
if (_identity is not null)
_identity.Updated -= update;
if (value is not null)
value.Updated += update;
_identity = value;
update();
}
}
[JsonIgnore]
public Locale Locale => IdentityTokens?.Locale;
public Account(string accountId)
{
AccountId = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountId, nameof(accountId)).Trim();
}
public override string ToString() => $"{AccountId} - {Locale?.Name ?? "[empty]"}";
public string MaskedLogEntry => @$"AccountId={mask(AccountId)}|AccountName={mask(AccountName)}|Locale={Locale?.Name ?? "[empty]"}";
private static string mask(string str)
=> str is null ? "[null]"
: str == string.Empty ? "[empty]"
: str.ToMask();
}
// whether to include this account when scanning libraries.
// technically this is an app setting; not an attribute of account. but since it's managed with accounts, it makes sense to put this exception-to-the-rule here
public bool LibraryScan
{
get => field;
set
{
if (value == field)
return;
field = value;
update();
}
}
/// <summary>aka: activation bytes</summary>
[AllowNull]
public string? DecryptKey
{
get => field;
set
{
var v = (value ?? "").Trim();
if (v == field)
return;
field = v;
update();
}
}
public Identity? IdentityTokens
{
get => field;
set
{
if (field is null && value is null)
return;
if (field is not null)
field.Updated -= update;
if (value is not null)
value.Updated += update;
field = value;
update();
}
}
[JsonIgnore]
public Locale? Locale => IdentityTokens?.Locale;
public Account(string accountId)
{
AccountId = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountId, nameof(accountId)).Trim();
}
public override string ToString() => $"{AccountId} - {Locale?.Name ?? "[empty]"}";
public string MaskedLogEntry => @$"AccountId={mask(AccountId)}|AccountName={mask(AccountName)}|Locale={Locale?.Name ?? "[empty]"}";
private static string mask(string? str)
=> str is null ? "[null]"
: str == string.Empty ? "[empty]"
: str.ToMask();
}
+142 -126
View File
@@ -1,142 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApi;
using AudibleApi;
using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
namespace AudibleUtilities
namespace AudibleUtilities;
// 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended
// from newtonsoft (https://www.newtonsoft.com/json/help/html/SerializationGuide.htm):
// .NET : IList, IEnumerable, IList<T>, Array
// JSON : Array (properties on the collection will not be serialized)
public class AccountsSettings : IUpdatable
{
// 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended
// from newtonsoft (https://www.newtonsoft.com/json/help/html/SerializationGuide.htm):
// .NET : IList, IEnumerable, IList<T>, Array
// JSON : Array (properties on the collection will not be serialized)
public class AccountsSettings : IUpdatable
public event EventHandler? Updated;
private void update(object? sender = null, EventArgs? e = null)
{
public event EventHandler Updated;
private void update(object sender = null, EventArgs e = null)
{
foreach (var account in Accounts)
validate(account);
update_no_validate();
}
private void update_no_validate() => Updated?.Invoke(this, new EventArgs());
public AccountsSettings() { }
// for some reason this will make the json instantiator use _accounts_json.set()
[JsonConstructor]
protected AccountsSettings(List<Account> accountsSettings) { }
#region Accounts
private List<Account> _accounts_backing = new List<Account>();
[JsonProperty(PropertyName = nameof(Accounts))]
private List<Account> _accounts_json
{
get => _accounts_backing;
// 'set' is only used by json deser
set
{
if (value is null)
return;
foreach (var account in value)
_add(account);
update_no_validate();
}
}
[JsonIgnore]
public IReadOnlyList<Account> Accounts => _accounts_json.AsReadOnly();
#endregion
#region de/serialize
public static AccountsSettings FromJson(string json)
=> JsonConvert.DeserializeObject<AccountsSettings>(json, Identity.GetJsonSerializerSettings());
public string ToJson(Formatting formatting = Formatting.Indented)
=> JsonConvert.SerializeObject(this, formatting, Identity.GetJsonSerializerSettings());
#endregion
// more common naming convention alias for internal collection
public IReadOnlyList<Account> GetAll() => Accounts;
public Account Upsert(string accountId, string locale)
{
var acct = GetAccount(accountId, locale);
if (acct is not null)
return acct;
var l = Localization.Get(locale);
var id = new Identity(l);
var account = new Account(accountId) { IdentityTokens = id };
Add(account);
return account;
}
public void Add(Account account)
{
_add(account);
update_no_validate();
}
public void _add(Account account)
{
foreach (var account in Accounts)
validate(account);
update_no_validate();
}
private void update_no_validate() => Updated?.Invoke(this, new EventArgs());
_accounts_backing.Add(account);
account.Updated += update;
}
public AccountsSettings() { }
public Account GetAccount(string accountId, string locale)
// for some reason this will make the json instantiator use _accounts_json.set()
[JsonConstructor]
protected AccountsSettings(List<Account> accountsSettings) { }
#region Accounts
private readonly List<Account> _accounts_backing = new List<Account>();
[JsonProperty(PropertyName = nameof(Accounts))]
private List<Account> _accounts_json
{
get => _accounts_backing;
// 'set' is only used by json deser
set
{
if (locale is null)
return null;
if (value is null)
return;
return Accounts.SingleOrDefault(a => a.AccountId == accountId && a.IdentityTokens.Locale.Name == locale);
}
foreach (var account in value)
_add(account);
public bool Delete(string accountId, string locale)
{
var acct = GetAccount(accountId, locale);
if (acct is null)
return false;
return Delete(acct);
}
public bool Delete(Account account)
{
if (!_accounts_backing.Contains(account))
return false;
account.Updated -= update;
var result = _accounts_backing.Remove(account);
update_no_validate();
return result;
}
private void validate(Account account)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
var accountId = account.AccountId;
var locale = account?.IdentityTokens?.Locale?.Name;
var acct = GetAccount(accountId, locale);
// new: ok
if (acct is null)
return;
// same account instance: ok
if (acct == account)
return;
// same account id + locale, different instance: bad
throw new InvalidOperationException("Cannot add an account with the same account Id and Locale");
}
}
private string? _cdm;
[JsonProperty]
public string? Cdm
{
get => _cdm;
set
{
if (value is null)
return;
_cdm = value;
update_no_validate();
}
}
[JsonIgnore]
public IReadOnlyList<Account> Accounts => _accounts_json.AsReadOnly();
#endregion
#region de/serialize
public static AccountsSettings? FromJson(string json)
=> JsonConvert.DeserializeObject<AccountsSettings>(json, Identity.GetJsonSerializerSettings());
public string ToJson(Formatting formatting = Formatting.Indented)
=> JsonConvert.SerializeObject(this, formatting, Identity.GetJsonSerializerSettings());
#endregion
// more common naming convention alias for internal collection
public IReadOnlyList<Account> GetAll() => Accounts;
public Account Upsert(string accountId, string? locale)
{
var acct = GetAccount(accountId, locale);
if (acct is not null)
return acct;
var l = Localization.Get(locale);
var id = new Identity(l);
// Match GUI default for new rows (WinForms/Avalonia): include account in library scans.
var account = new Account(accountId) { IdentityTokens = id, LibraryScan = true };
Add(account);
return account;
}
public void Add(Account account)
{
_add(account);
update_no_validate();
}
public void _add(Account account)
{
validate(account);
_accounts_backing.Add(account);
account.Updated += update;
}
public Account? GetAccount(string accountId, string? locale)
{
if (locale is null)
return null;
return Accounts.SingleOrDefault(a => a.AccountId == accountId && a.Locale?.Name == locale);
}
public bool Delete(string accountId, string locale)
{
var acct = GetAccount(accountId, locale);
if (acct is null)
return false;
return Delete(acct);
}
public bool Delete(Account account)
{
if (!_accounts_backing.Contains(account))
return false;
account.Updated -= update;
var result = _accounts_backing.Remove(account);
update_no_validate();
return result;
}
private void validate(Account account)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
var accountId = account.AccountId;
var locale = account?.IdentityTokens?.Locale?.Name;
var acct = GetAccount(accountId, locale);
// new: ok
if (acct is null)
return;
// same account instance: ok
if (acct == account)
return;
// same account id + locale, different instance: bad
throw new InvalidOperationException("Cannot add an account with the same account Id and Locale");
}
}
@@ -1,30 +1,29 @@
using System;
using AudibleApi.Authorization;
using AudibleApi.Authorization;
using Dinah.Core.IO;
using Newtonsoft.Json;
using System;
namespace AudibleUtilities
namespace AudibleUtilities;
public class AccountsSettingsPersister : JsonFilePersister<AccountsSettings>
{
public class AccountsSettingsPersister : JsonFilePersister<AccountsSettings>
{
public static event EventHandler Saving;
public static event EventHandler Saved;
public static event EventHandler? Saving;
public static event EventHandler? Saved;
protected override void OnSaving() => Saving?.Invoke(null, null);
protected override void OnSaved() => Saved?.Invoke(null, null);
protected override void OnSaving() => Saving?.Invoke(null, EventArgs.Empty);
protected override void OnSaved() => Saved?.Invoke(null, EventArgs.Empty);
/// <summary>Alias for Target </summary>
public AccountsSettings AccountsSettings => Target;
/// <summary>Alias for Target </summary>
public AccountsSettings AccountsSettings => Target;
/// <summary>uses path. create file if doesn't yet exist</summary>
public AccountsSettingsPersister(AccountsSettings target, string path, string jsonPath = null)
: base(target, path, jsonPath) { }
/// <summary>uses path. create file if doesn't yet exist</summary>
public AccountsSettingsPersister(AccountsSettings target, string path, string? jsonPath = null)
: base(target, path, jsonPath) { }
/// <summary>load from existing file</summary>
public AccountsSettingsPersister(string path, string jsonPath = null)
: base(path, jsonPath) { }
/// <summary>load from existing file</summary>
public AccountsSettingsPersister(string path, string? jsonPath = null)
: base(path, jsonPath) { }
protected override JsonSerializerSettings GetSerializerSettings()
=> Identity.GetJsonSerializerSettings();
}
protected override JsonSerializerSettings GetSerializerSettings()
=> Identity.GetJsonSerializerSettings();
}
+253 -257
View File
@@ -1,316 +1,312 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi;
using AudibleApi.Common;
using Dinah.Core;
using LibationFileManager;
using Newtonsoft.Json.Linq;
using Polly;
using Polly.Retry;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace AudibleUtilities
namespace AudibleUtilities;
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public class ApiExtended
{
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public class ApiExtended
public static Func<Account, ILoginChoiceEager>? LoginChoiceFactory { get; set; }
public Api Api { get; private set; }
private const int MaxConcurrency = 10;
private const int BatchSize = 50;
private ApiExtended(Api api) => Api = api;
/// <summary>Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks.</summary>
public static async Task<ApiExtended> CreateAsync(Account account)
{
public Api Api { get; private set; }
ArgumentValidator.EnsureNotNull(account, nameof(account));
ArgumentValidator.EnsureNotNull(account.AccountId, nameof(account.AccountId));
var locale = ArgumentValidator.EnsureNotNull(account.Locale, nameof(account.Locale));
private ApiExtended(Api api) => Api = api;
/// <summary>Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks.</summary>
public static async Task<ApiExtended> CreateAsync(Account account, ILoginChoiceEager loginChoiceEager)
try
{
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
LoginType = nameof(ILoginChoiceEager),
Account = account?.MaskedLogEntry ?? "[null]",
LocaleName = account?.Locale?.Name
});
var api = await EzApiCreator.GetApiAsync(
loginChoiceEager,
account.Locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath());
return new ApiExtended(api);
}
/// <summary>Get api from existing tokens else login with native api callbacks.</summary>
public static async Task<ApiExtended> CreateAsync(Account account, ILoginCallback loginCallback)
{
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
LoginType = nameof(ILoginCallback),
Account = account?.MaskedLogEntry ?? "[null]",
LocaleName = account?.Locale?.Name
});
var api = await EzApiCreator.GetApiAsync(
loginCallback,
account.Locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath());
return new ApiExtended(api);
}
/// <summary>Get api from existing tokens else login with external browser</summary>
public static async Task<ApiExtended> CreateAsync(Account account, ILoginExternal loginExternal)
{
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
LoginType = nameof(ILoginExternal),
Account = account?.MaskedLogEntry ?? "[null]",
LocaleName = account?.Locale?.Name
});
var api = await EzApiCreator.GetApiAsync(
loginExternal,
account.Locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath());
return new ApiExtended(api);
}
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
public static async Task<ApiExtended> CreateAsync(Account account)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
ArgumentValidator.EnsureNotNull(account.Locale, nameof(account.Locale));
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
AccountMaskedLogEntry = account.MaskedLogEntry
});
return await CreateAsync(account.AccountId, account.Locale.Name);
var api = await EzApiCreator.GetApiAsync(
locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath());
return new ApiExtended(api);
}
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
public static async Task<ApiExtended> CreateAsync(string username, string localeName)
catch
{
if (LoginChoiceFactory is null)
throw new InvalidOperationException($"The UI module must first set {nameof(LoginChoiceFactory)} before attempting to create the api");
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
Username = username.ToMask(),
LocaleName = localeName,
LoginType = nameof(ILoginChoiceEager),
Account = account.MaskedLogEntry ?? "[null]",
LocaleName = locale.Name
});
var api = await EzApiCreator.GetApiAsync(
Localization.Get(localeName),
AudibleApiStorage.AccountsSettingsFile,
AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName));
LoginChoiceFactory(account),
locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath());
return new ApiExtended(api);
}
}
private static AsyncRetryPolicy policy { get; }
= Policy.Handle<Exception>()
// 2 retries == 3 total
.RetryAsync(2);
private static AsyncRetryPolicy policy { get; }
= Policy.Handle<Exception>()
// 2 retries == 3 total
.RetryAsync(2);
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions libraryOptions, bool importEpisodes = true)
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions libraryOptions)
{
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or not tokens are refreshed
// worse, this 1st dummy call doesn't seem to help:
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
return policy.ExecuteAsync(() => getItemsAsync(libraryOptions));
}
/// <summary>
/// A debugging method used to simulate a library scan from a LibraryScans.zip json file.
/// Simply replace the Api call to GetLibraryItemsPagesAsync() with a call to this method.
/// </summary>
private static async IAsyncEnumerable<Item[]> GetItemsFromJsonFile()
{
var libraryScanJsonPath = @"Path/to/libraryscan.json";
using var jsonFile = System.IO.File.OpenText(libraryScanJsonPath);
var json = await JToken.ReadFromAsync(new Newtonsoft.Json.JsonTextReader(jsonFile));
if (json?["Items"] is not JArray items)
yield break;
foreach (var batch in items.OfType<JObject>().Select(Item.FromJson).OfType<Item>().Chunk(BatchSize))
yield return batch;
}
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions)
{
Serilog.Log.Logger.Debug("Beginning library scan.");
List<Item> items = new();
var sw = Stopwatch.StartNew();
var totalTime = TimeSpan.Zero;
using var semaphore = new SemaphoreSlim(MaxConcurrency);
var episodeChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
var batchReaderTask = readAllAsinsAsync(episodeChannel.Reader, semaphore);
//Scan the library for all added books.
//Get relationship asins from episode-type items and write them to episodeChannel where they will be batched and queried.
await foreach (var itemsBatch in Api.GetLibraryItemsPagesAsync(libraryOptions, BatchSize, semaphore))
{
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or not tokens are refreshed
// worse, this 1st dummy call doesn't seem to help:
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
return policy.ExecuteAsync(() => getItemsAsync(libraryOptions, importEpisodes));
if (Configuration.Instance.ImportEpisodes)
{
var episodes = itemsBatch.Where(i => i.IsEpisodes).ToList();
var series = itemsBatch.Where(i => i.IsSeriesParent).ToList();
var parentAsins = episodes
.SelectMany(i => i.Relationships ?? [])
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
.Select(r => r.Asin)
.OfType<string>();
var episodeAsins = series
.SelectMany(i => i.Relationships ?? [])
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
.Select(r => r.Asin)
.OfType<string>();
foreach (var asin in parentAsins.Concat(episodeAsins))
episodeChannel.Writer.TryWrite(asin);
items.AddRange(episodes);
items.AddRange(series);
}
var booksInBatch
= itemsBatch
.Where(i => !i.IsSeriesParent && !i.IsEpisodes)
.Where(i => i.IsAyce is not true || Configuration.Instance.ImportPlusTitles);
items.AddRange(booksInBatch);
}
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
sw.Stop();
totalTime += sw.Elapsed;
Serilog.Log.Logger.Debug("Library scan complete after {elappsed_ms} ms. Found {count} books and series. Waiting on series episode scans to complete.", sw.ElapsedMilliseconds, items.Count);
sw.Restart();
//Signal that we're done adding asins
episodeChannel.Writer.Complete();
//Wait for all episodes/parents to be retrived
var allEps = await batchReaderTask;
sw.Stop();
totalTime += sw.Elapsed;
Serilog.Log.Logger.Debug("Episode scan complete after {elappsed_ms} ms. Found {count} episodes and series .", sw.ElapsedMilliseconds, allEps.Count);
sw.Restart();
Serilog.Log.Logger.Debug("Begin indexing series episodes");
items.AddRange(allEps);
//Set the Item.Series info for episodes and parents.
foreach (var parent in items.Where(i => i.IsSeriesParent))
{
var items = new List<Item>();
#if DEBUG
//// this will not work for multi accounts
//var library_json = "library.json";
//library_json = System.IO.Path.GetFullPath(library_json);
//if (System.IO.File.Exists(library_json))
//{
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
//}
#endif
var children = items.Where(i => i.IsEpisodes && i.Relationships?.Any(r => r.Asin == parent.Asin) is true);
SetSeries(parent, children);
}
Serilog.Log.Logger.Debug("Begin initial library scan");
int orphansRemoved = items.RemoveAll(i => (i.IsEpisodes || i.IsSeriesParent) && i.Series is null);
if (orphansRemoved > 0)
Serilog.Log.Debug("{orphansRemoved} podcast orphans not imported", orphansRemoved);
if (!items.Any())
items = await Api.GetAllLibraryItemsAsync(libraryOptions);
sw.Stop();
totalTime += sw.Elapsed;
Serilog.Log.Logger.Information("Completed indexing series episodes after {elappsed_ms} ms.", sw.ElapsedMilliseconds);
Serilog.Log.Logger.Information($"Completed library scan in {totalTime.TotalMilliseconds:F0} ms.");
Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan");
Array.ForEach(ISanitizer.GetAllSanitizers(), s => s.Sanitize(items));
var allExceptions = IValidator.GetAllValidators().SelectMany(v => v.Validate(items)).ToList();
if (allExceptions?.Count > 0)
throw new ImportValidationException(items, allExceptions);
await manageEpisodesAsync(items, importEpisodes);
return items;
}
Serilog.Log.Logger.Debug("Episode scan complete");
#region episodes and podcasts
#if DEBUG
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
#endif
/// <summary>
/// Read asins from the channel and request catalog item info in batches of <see cref="BatchSize"/>. Blocks until <paramref name="channelReader"/> is closed.
/// </summary>
/// <param name="channelReader">Input asins to batch</param>
/// <param name="semaphore">Shared semaphore to limit concurrency</param>
/// <returns>All <see cref="Item"/>s of asins written to the channel.</returns>
private async Task<List<Item>> readAllAsinsAsync(ChannelReader<string> channelReader, SemaphoreSlim semaphore)
{
int batchNum = 1;
List<Task<List<Item>>> getTasks = new();
var validators = new List<IValidator>();
validators.AddRange(getValidators());
foreach (var v in validators)
while (await channelReader.WaitToReadAsync())
{
List<string> asins = new();
while (asins.Count < BatchSize && await channelReader.WaitToReadAsync())
{
var exceptions = v.Validate(items);
if (exceptions is not null && exceptions.Any())
throw new AggregateException(exceptions);
var asin = await channelReader.ReadAsync();
if (!asins.Contains(asin))
asins.Add(asin);
}
await semaphore.WaitAsync();
getTasks.Add(getProductsAsync(batchNum++, asins, semaphore));
}
var completed = await Task.WhenAll(getTasks);
//We only want Series parents and Series episodes. Exclude other relationship types (e.g. 'season')
return completed.SelectMany(l => l).Where(i => i.IsSeriesParent || i.IsEpisodes).ToList();
}
private async Task<List<Item>> getProductsAsync(int batchNum, List<string> asins, SemaphoreSlim semaphore)
{
Serilog.Log.Logger.Debug($"Batch {batchNum} Begin: Fetching {asins.Count} asins");
try
{
var sw = Stopwatch.StartNew();
var items = await Api.GetCatalogProductsAsync(asins, CatalogOptions.ResponseGroupOptions.Rating | CatalogOptions.ResponseGroupOptions.Media
| CatalogOptions.ResponseGroupOptions.Relationships | CatalogOptions.ResponseGroupOptions.ProductDesc
| CatalogOptions.ResponseGroupOptions.Contributors | CatalogOptions.ResponseGroupOptions.ProvidedReview
| CatalogOptions.ResponseGroupOptions.ProductPlans | CatalogOptions.ResponseGroupOptions.Series
| CatalogOptions.ResponseGroupOptions.CategoryLadders | CatalogOptions.ResponseGroupOptions.ProductExtendedAttrs);
sw.Stop();
Serilog.Log.Logger.Debug($"Batch {batchNum} End: Retrieved {items.Count} items in {sw.ElapsedMilliseconds} ms");
return items;
}
#region episodes and podcasts
private async Task manageEpisodesAsync(List<Item> items, bool importEpisodes)
catch (Exception ex)
{
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
try
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new { asins });
throw;
}
finally { semaphore.Release(); }
}
public static void SetSeries(Item parent, IEnumerable<Item> children)
{
ArgumentValidator.EnsureNotNull(parent, nameof(parent));
ArgumentValidator.EnsureNotNull(children, nameof(children));
//A series parent will always have exactly 1 Series
parent.Series = new[]
{
new Series
{
// get parents
var parents = items.Where(i => i.IsEpisodes).ToList();
#if DEBUG
//var parentsDebug = parents.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText("parents.json", parentsDebug);
#endif
if (!parents.Any())
return;
Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found");
// remove episode parents. even if the following stuff fails, these will still be removed from the collection
items.RemoveAll(i => i.IsEpisodes);
if (importEpisodes)
{
// add children
var children = await getEpisodesAsync(parents);
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
items.AddRange(children);
}
Asin = parent.Asin,
Sequence = "-1",
Title = parent.TitleWithSubtitle
}
catch (Exception ex)
};
if (parent.PurchaseDate == default)
{
parent.PurchaseDate = children.Select(c => c.PurchaseDate).Order().FirstOrDefault(d => d != default);
if (parent.PurchaseDate == default)
{
Serilog.Log.Logger.Error(ex, "Error adding podcasts and episodes");
Serilog.Log.Logger.Warning("{series} doesn't have a purchase date. Using UtcNow", parent);
parent.PurchaseDate = DateTimeOffset.UtcNow;
}
}
private async Task<List<Item>> getEpisodesAsync(List<Item> parents)
int lastEpNum = -1, dupeCount = 0;
foreach (var child in children.OrderBy(i => i.EpisodeNumber).ThenBy(i => i.PublicationDateTime))
{
var results = new List<Item>();
foreach (var parent in parents)
string sequence;
if (child.EpisodeNumber is null)
{
var children = await getEpisodeChildrenAsync(parent);
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
sequence = parent.Relationships?.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0";
}
else
{
//multipart episodes may have the same episode number
if (child.EpisodeNumber == lastEpNum)
dupeCount++;
else
lastEpNum = child.EpisodeNumber.Value;
// actual individual episode, not the parent of a series.
// for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic
if (!children.Any())
{
results.Add(parent);
continue;
}
foreach (var child in children)
{
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
child.PurchaseDate = parent.PurchaseDate;
// parent is essentially a series
child.Series = new Series[]
{
new Series
{
Asin = parent.Asin,
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin).Sort.ToString(),
Title = parent.TitleWithSubtitle
}
};
// overload (read: abuse) IsEpisodes flag
child.Relationships = new Relationship[]
{
new Relationship
{
RelationshipToProduct = RelationshipToProduct.Child,
RelationshipType = RelationshipType.Episode
}
};
}
results.AddRange(children);
sequence = (lastEpNum + dupeCount).ToString();
}
return results;
}
private async Task<List<Item>> getEpisodeChildrenAsync(Item parent)
{
var childrenIds = parent.Relationships
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
.Select(r => r.Asin)
.ToList();
// fetch children in batches
const int batchSize = 20;
var results = new List<Item>();
for (var i = 1; ; i++)
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
child.PurchaseDate = parent.PurchaseDate;
// parent is essentially a series
child.Series = new[]
{
var idBatch = childrenIds.Skip((i - 1) * batchSize).Take(batchSize).ToList();
if (!idBatch.Any())
break;
List<Item> childrenBatch;
try
new Series
{
childrenBatch = await Api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
#if DEBUG
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
#endif
Asin = parent.Asin,
Sequence = sequence,
Title = parent.TitleWithSubtitle
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new
{
ParentId = parent.Asin,
ParentTitle = parent.Title,
BatchNumber = i,
ChildIdBatch = idBatch
});
throw;
}
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results");
// the service returned no results. probably indicates an error. stop running batches
if (!childrenBatch.Any())
break;
results.AddRange(childrenBatch);
}
Serilog.Log.Logger.Debug("Parent episodes/podcasts series. Children found. {@DebugInfo}", new
{
ParentId = parent.Asin,
ParentTitle = parent.Title,
ChildCount = childrenIds.Count
});
if (childrenIds.Count != results.Count)
{
var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}");
Serilog.Log.Logger.Error(ex, "Quantity of series episodes defined by parent does not match quantity returned by batch fetching.");
throw ex;
}
return results;
}
#endregion
private static List<IValidator> getValidators()
{
var type = typeof(IValidator);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && !p.IsInterface);
return types.Select(t => Activator.CreateInstance(t) as IValidator).ToList();
};
}
}
#endregion
}
@@ -0,0 +1,32 @@
using AudibleApi.Common;
using System.Collections.Generic;
using System.Linq;
namespace AudibleUtilities;
public interface ISanitizer
{
void Sanitize(IEnumerable<Item> items);
public static ISanitizer[] GetAllSanitizers() => [
new ContributorSanitizer()
];
}
public class ContributorSanitizer : ISanitizer
{
public void Sanitize(IEnumerable<Item> items)
{
foreach (var item in items)
{
item.Authors = SanitizePersonArray(item.Authors);
item.Narrators = SanitizePersonArray(item.Narrators);
}
}
private static Person[]? SanitizePersonArray(Person?[]? contributors)
=> contributors
?.OfType<Person>()
.Where(c => !string.IsNullOrWhiteSpace(c.Name))
.ToArray();
}
+71 -41
View File
@@ -1,47 +1,77 @@
using System;
using System.IO;
using LibationFileManager;
using LibationFileManager;
using Newtonsoft.Json;
using System;
using System.IO;
namespace AudibleUtilities
namespace AudibleUtilities;
public class AccountSettingsLoadErrorEventArgs : ErrorEventArgs
{
public static class AudibleApiStorage
/// <summary>
/// Create a new, empty <see cref="AccountsSettings"/> file if true, otherwise throw
/// </summary>
public bool Handled { get; set; }
/// <summary>
/// The file path of the AccountsSettings.json file
/// </summary>
public string SettingsFilePath { get; }
public AccountSettingsLoadErrorEventArgs(string path, Exception exception)
: base(exception)
{
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
public static void EnsureAccountsSettingsFileExists()
{
// saves. BEWARE: this will overwrite an existing file
if (!File.Exists(AccountsSettingsFile))
_ = new AccountsSettingsPersister(new AccountsSettings(), AccountsSettingsFile);
}
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
public static AccountsSettingsPersister GetAccountsSettingsPersister() => new AccountsSettingsPersister(AccountsSettingsFile);
public static string GetIdentityTokensJsonPath(this Account account)
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
public static string GetIdentityTokensJsonPath(string username, string localeName)
{
var usernameSanitized = trimSurroundingQuotes(JsonConvert.ToString(username));
var localeNameSanitized = trimSurroundingQuotes(JsonConvert.ToString(localeName));
return $"$.Accounts[?(@.AccountId == '{usernameSanitized}' && @.IdentityTokens.LocaleName == '{localeNameSanitized}')].IdentityTokens";
}
private static string trimSurroundingQuotes(string str)
{
// SubString algo is better than .Trim("\"")
// orig string "
// json string "\""
// Eg:
// => str.Trim("\"")
// output \
// vs
// => str.Substring(1, str.Length - 2)
// output \"
// also works with surrounding single quotes
return str.Substring(1, str.Length - 2);
}
SettingsFilePath = path;
}
}
public static class AudibleApiStorage
{
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles.Location, "AccountsSettings.json");
public static event EventHandler<AccountSettingsLoadErrorEventArgs>? LoadError;
public static void EnsureAccountsSettingsFileExists()
{
// saves. BEWARE: this will overwrite an existing file
if (!File.Exists(AccountsSettingsFile))
{
//Save the JSON file manually so that AccountsSettingsPersister.Saving and AccountsSettingsPersister.Saved
//are not fired. There's no need to fire those events on an empty AccountsSettings file.
var accountSerializerSettings = AudibleApi.Authorization.Identity.GetJsonSerializerSettings();
File.WriteAllText(AccountsSettingsFile, JsonConvert.SerializeObject(new AccountsSettings(), Formatting.Indented, accountSerializerSettings));
}
}
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
public static AccountsSettingsPersister GetAccountsSettingsPersister()
{
try
{
return new AccountsSettingsPersister(AccountsSettingsFile);
}
catch (Exception ex)
{
var args = new AccountSettingsLoadErrorEventArgs(AccountsSettingsFile, ex);
LoadError?.Invoke(null, args);
if (args.Handled)
return GetAccountsSettingsPersister();
throw;
}
}
public static string GetIdentityTokensJsonPath(this Account account)
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
public static string GetIdentityTokensJsonPath(string username, string? localeName)
{
var usernameEscaped = EscapeNewtonsoftJsonPathSingleQuotedLiteral(username);
var localeNameEscaped = EscapeNewtonsoftJsonPathSingleQuotedLiteral(localeName ?? string.Empty);
return $"$.Accounts[?(@.AccountId == '{usernameEscaped}' && @.IdentityTokens.LocaleName == '{localeNameEscaped}')].IdentityTokens";
}
/// <summary>
/// Escape a value for use inside single-quoted Newtonsoft JSONPath filter literals.
/// See https://www.newtonsoft.com/json/help/html/QueryJsonSelectTokenEscaped.htm
/// </summary>
internal static string EscapeNewtonsoftJsonPathSingleQuotedLiteral(string value)
=> value.Replace("\\", @"\\").Replace("'", @"\'");
}
+82 -79
View File
@@ -1,88 +1,91 @@
using System;
using AudibleApi.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
namespace AudibleUtilities
namespace AudibleUtilities;
public interface IValidator
{
public interface IValidator
IEnumerable<Exception> Validate(IEnumerable<Item> items);
public static IValidator[] GetAllValidators() => [
new LibraryValidator(),
new BookValidator(),
new CategoryValidator(),
new SeriesValidator(),
];
}
/// <summary>
/// To be used when no validation is desired
/// </summary>
public class ClearValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items) => [];
}
public class LibraryValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
IEnumerable<Exception> Validate(IEnumerable<Item> items);
}
public class LibraryValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
var exceptions = new List<Exception>();
var exceptions = new List<Exception>();
if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId)))
exceptions.Add(new ArgumentException($"Collection contains item(s) with null or blank {nameof(Item.ProductId)}", nameof(items)));
if (items.Any(i => i.DateAdded < new DateTime(1980, 1, 1)))
exceptions.Add(new ArgumentException($"Collection contains item(s) with invalid {nameof(Item.DateAdded)}", nameof(items)));
if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId)))
exceptions.Add(new ArgumentException($"Collection contains item(s) with null or blank {nameof(Item.ProductId)}", nameof(items)));
//// unfortunately, an actual user has a title with a beginning-of-time 'purchase_date'
//if (items.Any(i => i.DateAdded < new DateTime(1980, 1, 1)))
// exceptions.Add(new ArgumentException($"Collection contains item(s) with invalid {nameof(Item.DateAdded)}", nameof(items)));
return exceptions;
}
}
public class BookValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
var exceptions = new List<Exception>();
// a book having no authors is rare but allowed
if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId)))
exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.ProductId)}", nameof(items)));
// this can happen with podcast episodes
foreach (var i in items.Where(i => string.IsNullOrWhiteSpace(i.Title)))
i.Title = "[blank title]";
return exceptions;
}
}
public class CategoryValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
var exceptions = new List<Exception>();
var distinct = items.GetCategoriesDistinct();
if (distinct.Any(s => s.CategoryId is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with null {nameof(Ladder.CategoryId)}", nameof(items)));
if (distinct.Any(s => s.CategoryName is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with null {nameof(Ladder.CategoryName)}", nameof(items)));
return exceptions;
}
}
public class ContributorValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
var exceptions = new List<Exception>();
if (items.GetAuthorsDistinct().Any(a => string.IsNullOrWhiteSpace(a.Name)))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Authors)} with null {nameof(Person.Name)}", nameof(items)));
if (items.GetNarratorsDistinct().Any(a => string.IsNullOrWhiteSpace(a.Name)))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Narrators)} with null {nameof(Person.Name)}", nameof(items)));
return exceptions;
}
}
public class SeriesValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
var exceptions = new List<Exception>();
var distinct = items.GetSeriesDistinct();
if (distinct.Any(s => s.SeriesId is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesId)}", nameof(items)));
if (distinct.Any(s => s.SeriesName is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesName)}", nameof(items)));
return exceptions;
}
return exceptions;
}
}
public class BookValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
var exceptions = new List<Exception>();
// a book having no authors is rare but allowed
if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId)))
exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.ProductId)}", nameof(items)));
// this can happen with podcast episodes
foreach (var i in items.Where(i => string.IsNullOrWhiteSpace(i.Title)))
i.Title = "[blank title]";
return exceptions;
}
}
public class CategoryValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
var exceptions = new List<Exception>();
var distinct = items.GetCategoriesDistinct();
if (distinct.Any(s => s.CategoryId is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with null {nameof(Ladder.CategoryId)}", nameof(items)));
if (distinct.Any(s => s.CategoryName is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with null {nameof(Ladder.CategoryName)}", nameof(items)));
return exceptions;
}
}
public class SeriesValidator : IValidator
{
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
{
var exceptions = new List<Exception>();
var distinct = items.GetSeriesDistinct();
if (distinct.Any(s => s.SeriesId is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesId)}", nameof(items)));
//// unfortunately, an actual user has a series with no name
//if (distinct.Any(s => s.SeriesName is null))
// exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesName)}", nameof(items)));
return exceptions;
}
}
@@ -1,15 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="2.8.4.1" />
<PackageReference Include="AudibleApi" Version="10.2.0.1" />
<PackageReference Include="Google.Protobuf" Version="3.34.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LibationFileManager\LibationFileManager.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<Compile Update="Widevine\Cdm.*.cs">
<DependentUpon>Cdm.cs</DependentUpon>
</Compile>
</ItemGroup>
</Project>
Loaded 100 of 954 files, more files were not shown because too many files have changed in this diff. Show more