Compare commits

...

155 Commits

Author SHA1 Message Date
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
154 changed files with 2558 additions and 857 deletions

View File

@@ -8,37 +8,37 @@ on:
inputs:
version_override:
type: string
description: 'Version number override'
description: "Version number override"
required: false
run_unit_tests:
type: boolean
description: 'Skip running unit tests'
description: "Skip running unit tests"
required: false
default: true
runs_on:
type: string
description: 'The GitHub hosted runner to use'
description: "The GitHub hosted runner to use"
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
architecture:
type: string
description: 'CPU architecture targeted by the build.'
description: "CPU architecture targeted by the build."
required: true
env:
DOTNET_CONFIGURATION: 'Release'
DOTNET_VERSION: '8.0.x'
RELEASE_NAME: 'chardonnay'
DOTNET_CONFIGURATION: "Release"
DOTNET_VERSION: "9.0.x"
RELEASE_NAME: "chardonnay"
jobs:
build:
name: '${{ inputs.OS }}-${{ inputs.architecture }}'
name: "${{ inputs.OS }}-${{ inputs.architecture }}"
runs-on: ${{ inputs.runs_on }}
steps:
- uses: actions/checkout@v4
@@ -60,7 +60,7 @@ jobs:
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
fi
echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Unit test
if: ${{ inputs.run_unit_tests }}
working-directory: ./Source
@@ -69,7 +69,7 @@ jobs:
- name: Publish
id: publish
working-directory: ./Source
run: |
run: |
if [[ "${{ inputs.OS }}" == "MacOS" ]]
then
display_os="macOS"
@@ -78,13 +78,13 @@ jobs:
display_os="Linux"
RUNTIME_ID="linux-${{ inputs.architecture }}"
fi
OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}"
echo "display_os=${display_os}" >> $GITHUB_OUTPUT
echo "Runtime Identifier: $RUNTIME_ID"
echo "Output Directory: $OUTPUT"
dotnet publish \
LibationAvalonia/LibationAvalonia.csproj \
--runtime $RUNTIME_ID \
@@ -122,7 +122,7 @@ jobs:
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}"
artifact=$(ls ./bundle)
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
- name: Publish bundle
uses: actions/upload-artifact@v4
with:

View File

@@ -8,21 +8,21 @@ on:
inputs:
version_override:
type: string
description: 'Version number override'
description: "Version number override"
required: false
run_unit_tests:
type: boolean
description: 'Skip running unit tests'
description: "Skip running unit tests"
required: false
default: true
env:
DOTNET_CONFIGURATION: 'Release'
DOTNET_VERSION: '8.0.x'
DOTNET_CONFIGURATION: "Release"
DOTNET_VERSION: "9.0.x"
jobs:
build:
name: '${{ matrix.os }}-${{ matrix.release_name }}'
name: "${{ matrix.os }}-${{ matrix.release_name }}"
runs-on: windows-latest
strategy:
matrix:
@@ -112,4 +112,4 @@ jobs:
name: ${{ steps.zip.outputs.artifact }}.zip
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
if-no-files-found: error
retention-days: 7
retention-days: 7

View File

@@ -8,22 +8,21 @@ on:
inputs:
version_override:
type: string
description: 'Version number override'
description: "Version number override"
required: false
run_unit_tests:
type: boolean
description: 'Skip running unit tests'
description: "Skip running unit tests"
required: false
default: true
default: true
jobs:
windows:
uses: ./.github/workflows/build-windows.yml
with:
version_override: ${{ inputs.version_override }}
run_unit_tests: ${{ inputs.run_unit_tests }}
linux:
strategy:
matrix:
@@ -36,7 +35,7 @@ jobs:
OS: ${{ matrix.OS }}
architecture: ${{ matrix.architecture }}
run_unit_tests: ${{ inputs.run_unit_tests }}
macos:
strategy:
matrix:
@@ -47,4 +46,4 @@ jobs:
runs_on: macos-latest
OS: MacOS
architecture: ${{ matrix.architecture }}
run_unit_tests: ${{ inputs.run_unit_tests }}
run_unit_tests: ${{ inputs.run_unit_tests }}

View File

@@ -8,7 +8,11 @@ on:
inputs:
version:
type: string
description: 'Version number'
description: "Version number"
required: true
release:
type: boolean
description: "Is this a release build?"
required: true
secrets:
docker_username:
@@ -16,12 +20,10 @@ on:
docker_token:
required: true
env:
DOCKER_IMAGE: ${{ secrets.docker_username }}/libation
jobs:
docker:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -33,14 +35,29 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: ${{ inputs.release }}
uses: docker/login-action@v3
with:
username: ${{ secrets.docker_username }}
password: ${{ secrets.docker_token }}
- name: Build and push
uses: docker/build-push-action@v5
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
push: true
build-args: 'FOLDER_NAME=Linux-chardonnay'
tags: ${{ env.DOCKER_IMAGE }}:latest,${{ env.DOCKER_IMAGE }}:${{ inputs.version }}
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@v6
with:
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 }}

View File

@@ -5,7 +5,7 @@ name: release
on:
push:
tags:
- 'v*'
- "v*"
jobs:
prerelease:
runs-on: ubuntu-latest
@@ -15,7 +15,7 @@ jobs:
- name: Get tag version
id: get_version
run: |
export TAG='${{ github.ref_name }}'
export TAG="${{ github.ref_name }}"
echo "version=${TAG#v}" >> "${GITHUB_OUTPUT}"
docker:
@@ -23,6 +23,7 @@ jobs:
uses: ./.github/workflows/docker.yml
with:
version: ${{ needs.prerelease.outputs.version }}
release: true
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -33,29 +34,25 @@ jobs:
with:
version_override: ${{ needs.prerelease.outputs.version }}
run_unit_tests: false
release:
needs: [prerelease,build]
needs: [prerelease, build]
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: "*(Classic-)Libation.*"
- name: Release
id: release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
name: Libation v${{ needs.prerelease.outputs.version }}
name: Libation ${{ needs.prerelease.outputs.version }}
body: <Put a body here>
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
prerelease: false
- name: Upload release assets
uses: dwenegar/upload-release-assets@v2
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
with:
release_id: '${{ steps.release.outputs.id }}'
assets_path: ./artifacts
files: |
artifacts/*/*

View File

@@ -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@v4
- name: Check the MetaInfo file
run: flatpak-builder-lint appstream Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml

View File

@@ -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@v4
- run: sudo apt --yes install desktop-file-utils
- name: Check the desktop file
run: desktop-file-validate Source/LoadByOS/LinuxConfigApp/Libation.desktop

View File

@@ -1,5 +1,5 @@
# validate.yml
# Validates that Libation will build on a pull request or push to master.
# Validates that Libation will build on a pull request or push to master.
---
name: validate
@@ -12,3 +12,11 @@ on:
jobs:
build:
uses: ./.github/workflows/build.yml
docker:
uses: ./.github/workflows/docker.yml
with:
version: ${GITHUB_SHA}
release: false
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}

3
Docker/appsettings.json Normal file
View File

@@ -0,0 +1,3 @@
{
"LibationFiles": "/config-internal"
}

View File

@@ -1,68 +1,174 @@
#!/bin/bash
# Rewire echo to print date time
echo() {
if [[ -n $1 ]]; then
printf "$(date '+%F %T'): %s\n" "$1"
fi
error() {
log "ERROR" "$1"
}
# ################################
# Setup
# ################################
echo "Starting"
if [[ -z "${SLEEP_TIME}" ]]; then
echo "No sleep time passed in. Will run once and exit."
else
echo "Sleep time is set to ${SLEEP_TIME}"
fi
warn() {
log "WARNING" "$1"
}
echo ""
info() {
log "info" "$1"
}
# Check if the config directory is passed in, and there is no link to it then create the link.
if [ -d "/config" ] && [ ! -d "/root/Libation" ]; then
echo "Linking config directory to the Libation config directory"
ln -s /config/ /root/Libation
fi
debug() {
if [ "${LOG_LEVEL}" = "debug" ]; then
log "debug" "$1"
fi
}
# If no config error and exit
if [ ! -d "/config" ]; then
echo "ERROR: No /config directory. You must pass in a volume containing your config mapped to /config"
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() {
DBPATH=$1
dbpattern="*.db"
debug "using database directory ${DBPATH}"
# Figure out the right databse file
if [[ -z "${LIBATION_DB_FILE}" ]];
then
dbCount=$(find "${DBPATH}" -maxdepth 1 -type f -name "${dbpattern}" | wc -l)
if [ "${dbCount}" -gt 1 ];
then
error "too many database files found, set LIBATION_DB_FILE to the filename you wish to use"
exit 1
elif [ "${dbCount}" -eq 1 ];
then
files=( ${DBPATH}/${dbpattern} )
FILE=${files[0]}
else
FILE="${DBPATH}/LibationContext.db"
fi
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
fi
ln -s "${FILE}" "${LIBATION_CONFIG_INTERNAL}/LibationContext.db"
}
# If user passes in db from a /db/ folder and a db does not already exist / is not already linked
FILE=/db/LibationContext.db
if [ -f "${FILE}" ] && [ ! -f "/config/LibationContext.db" ]; then
echo "Linking passed in Libation database from /db/ to the Libation config directory"
ln -s $FILE /config/LibationContext.db
fi
run() {
info "scanning accounts"
/libation/LibationCli scan
info "liberating books"
/libation/LibationCli liberate
}
# Confirm we have a db in the config direcotry.
if [ ! -f "/config/LibationContext.db" ]; then
echo "ERROR: No Libation database detected, exiting."
exit 1
fi
main() {
info "initializing libation"
init_config_file AccountsSettings.json
init_config_file Settings.json
info "loading settings"
update_settings Settings.json Books /data
update_settings Settings.json InProgress /tmp
# ################################
# Loop and liberate
# ################################
while true
do
echo ""
echo "Scanning accounts"
/libation/LibationCli scan
echo "Liberating books"
/libation/LibationCli liberate
echo ""
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
if [ "${SLEEP_TIME}" == -1 ]; then
break
fi
echo "Sleeping for ${SLEEP_TIME}"
sleep "${SLEEP_TIME}"
done
done
echo "Exiting"
info "exiting"
}
main

View File

@@ -1,22 +1,39 @@
# Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:7.0 as build-env
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG TARGETARCH
COPY Source /Source
RUN dotnet publish -c Release -o /Source/bin/Publish/Linux-chardonnay /Source/LibationCli/LibationCli.csproj -p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml
COPY Docker/liberate.sh /Source/bin/Publish/Linux-chardonnay
RUN dotnet publish \
/Source/LibationCli/LibationCli.csproj \
--arch ${TARGETARCH} \
--configuration Release \
--output /Source/bin/Publish/Linux-chardonnay \
-p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml
FROM mcr.microsoft.com/dotnet/runtime:9.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
FROM mcr.microsoft.com/dotnet/runtime:7.0
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y jq && \
mkdir -m777 ${LIBATION_CONFIG_INTERNAL} ${LIBATION_BOOKS_DIR}
ENV SLEEP_TIME "30m"
COPY --from=build /Source/bin/Publish/Linux-chardonnay /libation
COPY Docker/* /libation
# Sets the character set that will be used for folder and filenames when liberating
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
USER ${USER_UID}:${USER_GID}
RUN mkdir /db /config /data
COPY --from=build-env /Source/bin/Publish/Linux-chardonnay /libation
CMD ["./libation/liberate.sh"]
CMD ["/libation/liberate.sh"]

View File

@@ -1,6 +1,6 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.

View File

@@ -1,31 +1,32 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
> [!WARNING]
> ## Breaking Changes
> * The docker image now runs as user 1001 and group 1001. Make sure that the permissions on your volumes allow user 1001 to read and write to them or see the User section below for other options, or if you're not sure.
> * `SLEEP_TIME` is now set to `-1` by default. This means the image will run once and exit. If you were relying on the previous default, you'll need to explicitly set the `SLEEP_TIME` environment variable to `30m` to replicate the previous behavior.
> * The docker image now ignores the values in `Settings.json` for `Books` and `InProgress`. You can now change the folder that books are saved to by using the `LIBATION_BOOKS_DIR` environment variable.
### Disclaimer
# Disclaimer
The docker image is provided as-is. We hope it can be useful to you but it is not officially supported.
### Setup
In order to use the docker image, you'll need to provide it with a copy of the `AccountsSettings.json`, `Settings.json`, and `LibationContext.db` files. These files can usually be found in the Libation folder in your user's home directory. If you haven't run Libation yet, you'll need to launch it to generate these files and setup your accounts. Once you have them, copy these files to a new location, such as `/opt/libation/config`. Before using them we'll need to make a couple edits so that the filepaths referenced are correct when running from the docker image.
In Settings.json, make the following changes:
* Change `Books` to `/data`
* Change `InProgress` to `/tmp`
### Configuration
Configuration in Libation is handled by two files, `AccountsSettings.json` and `Settings.json`. These files can usually be found in the Libation folder in your user's home directory. The easiest way to configure these is to run the desktop version of Libation and then copy them into a folder, such as `/opt/libation/config`, that you'll volume mount into the image. `Settings.json` is technically optional, and, if not provided, Libation will run using the default settings. Additionally, the `Books` and `InProgress` settings in `Settings.json` will be ignored and the image will instead substitute it's own values.
### Running
Once the configuration files are copied and edited, the docker image can be run with the following command.
Once the configuration files are copied, the docker image can be run with the following command.
```
sudo docker run -d \
-v /opt/libation/config:/config \
-v /opt/libation/books:/data \
--name libation \
--restart=always \
rmcrackan/libation
rmcrackan/libation:latest
```
By default the container will scan for new books every 30 minutes and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. Additionally, if you pass in `-1` it will scan and download books once and then exit.
By default the container will scan for new books once and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. For example, if you pass in `10m` it will keep running, scan for new books, and download them every 10 minutes.
```
sudo docker run -d \
@@ -34,6 +35,37 @@ sudo docker run -d \
-e SLEEP_TIME='10m' \
--name libation \
--restart=always \
rmcrackan/libation
rmcrackan/libation:latest
```
### Environment Variables
| Env Var | Default | Description |
| -------- | ------- | ----------- |
| SLEEP_TIME | -1 | Length of time to sleep before doing another scan/download. Set to -1 to run one. |
| LIBATION_BOOKS_DIR | /data | Folder where books will be saved |
| LIBATION_CONFIG_DIR | /config | Folder to read configuration from. |
| LIBATION_DB_DIR | /db | Optional folder to load database from. If not mounted, will load database from `LIBATION_CONFIG_DIR`. |
| LIBATION_DB_FILE | | Name of database file to load. By default it will look for all `.db` files and load one if there is only one present. |
| LIBATION_CREATE_DB | true | Whether or not the image should create a database file if none are found. |
### User
This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors, including [sqlite error](#1060), [Microsoft.Data.Sqlite.SqliteException](#1110), [unable to open database file](#1113), [Microsoft.EntityFrameworkCore.DbUpdateException](#1049)
If you're not sure what your user number is, check the output of the `id` command. Docker should normally run with the number of the user who configured and ran it.
If you want to change the user the image runs as, you can specify `-u <uid>:<gid>`. For example, to run it as user `2000` and group `3000`, you could do the following:
```
sudo docker run -d \
-u 2000:3000 \
-v /opt/libation/config:/config \
-v /opt/libation/books:/data \
--name libation \
--restart=always \
rmcrackan/libation:latest
```
If the user it's running as is correct, and it still cannot write, be sure to check whether the files and/or folders might be owned by the wrong user. You can use the `chown` command to change the owner of the file to the correct user and group number, for example: `chown -R 1001:1001 /mnt/audiobooks /mnt/libation-config`
### Advanced Database Options
The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. If the `LIBATION_DB_DIR` is mounted, it will be used, otherwise it will look for the database in `LIBATION_CONFIG_DIR`. If it does not find the database in the expected location, it will attempt to make an empty database there.

View File

@@ -1,16 +1,42 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
### Frequently Asked Questions
**Q: Now that I've downloaded my books, how can I listen to them?**
# Frequently Asked Questions
## Q: What's the difference between 'Classic' and 'Chardonnay'?
**A:** First and most importantly: Classic and Chardonnay have the exact same features.
* **Classic** is Windows only. Its older 'grey boxes' look has a compact design which allows for more information on the screen. Notably, Classic was written using an older, more mature technology which has built-in support for screenreaders.
* **Chardonnay** is available for Windows, Mac, and Linux. Its modern design has a more open look and feel.
## Q: Now that I've downloaded my books, how can I listen to them?
**A:** You can use any app which plays m4b files (or mp3 files if you used that setting). Here are just a few ideas. Disclaimer: I have no affiliation with any of these companies:
* iOS: [BookPlayer](https://apps.apple.com/us/app/bookplayer/id1138219998)
* iOS: [Bound](https://apps.apple.com/us/app/bound-audiobook-player/id1041727137)
* Android: [Smart AudioBook Player](https://play.google.com/store/apps/details?id=ak.alizandro.smartaudiobookplayer&hl=en_US&gl=US)
* Desktop: [VLC](https://www.videolan.org/)
* Android: [Listen](https://play.google.com/store/apps/details?id=ru.litres.android.audio&hl=en_US&gl=US)
* Desktop: [VLC](https://www.videolan.org/)
* Windows Desktop: [Audibly](https://github.com/rstewa/Audibly) -- a desktop player build specifically for audiobooks
Self-hosting online:
* [audiobookshelf](https://www.audiobookshelf.org). On [reddit](https://www.reddit.com/r/audiobookshelf/)
* [plex](https://www.plex.tv/). Listen with [Prologue](https://prologue.audio/) (iOS)
## Q: I'm having trouble loggin into my Brazil account.
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
## Q: How do I use Libation with a South Africa account?
**A:** Like many countries, amazon gives South Africa it's own amazon site. [Unlike many other regions](https://www.audible.com/ep/country-selector) there is not South Africa specific audible site. Use `US` for your region -- ie: audible.com.
(Not exactly a *frequently* asked question but it's come up more than once)

View File

@@ -1,6 +1,6 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.

View File

@@ -1,27 +1,59 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
### Install and Run Libation on Ubuntu
## Packaging status
New Libation releases are automatically packed into .deb and .rpm package and are available from the Libation repository's releases page.
[![Packaging status](https://repology.org/badge/vertical-allrepos/libation.svg)](https://repology.org/project/libation/versions)
New Libation releases are automatically packed into `.deb` and `.rpm` package and are available from the Libation repository's releases page.
Run this command in your terminal to dowbnload and install Libation, replacing the url with the latest Libation package url:
Run this command in your terminal to download and install Libation, replacing the url with the latest Libation package url:
- Debian
### Debian
```Console
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
sudo apt install ./libation.deb
```
- Redhat and CentOS
### Redhat and CentOS
```Console
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
sudo yum install ./libation.rpm
```
---
### Arch Linux
```Console
yay -S libation
```
This package is available on [Arch User Repository](https://aur.archlinux.org/packages/libation), install via your choice of [AUR helpers](https://wiki.archlinux.org/title/AUR_helpers).
Thanks to [mhdi](https://aur.archlinux.org/account/mhdi) for taking care of AUR package maintenance.
### NixOS
- Install via `nix-shell`
```Console
nix-shell -p libation
```
A `nix-shell` will temporarily modify your $PATH environment variable. This can be used to try a piece of software before deciding to permanently install it.
- Install via NixOS configuration
```Console
environment.systemPackages = [
pkgs.libation
];
```
Add the following Nix code to your NixOS Configuration, usually located in `/etc/nixos/configuration.nix`
- On NixOS via via `nix-env`
```Console
nix-env -iA nixos.libation
```
- On Non NixOS via `nix-env`
```Console
nix-env -iA nixpkgs.libation
```
Warning: Using `nix-env` permanently modifies a local profile of installed packages. This must be updated and maintained by the user in the same way as with a traditional package manager.
Thanks to [TomaSajt](https://github.com/tomasajt) for taking care of Nix package maintenance.
If your desktop uses gtk, you should now see Libation among your applications.

View File

@@ -1,9 +1,10 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
# Run Libation on MacOS
This walkthrough should get you up and running with Libation on your Mac.
@@ -12,16 +13,41 @@ This walkthrough should get you up and running with Libation on your Mac.
## Install Libation
- Download the file from the latest release and extract it.
- Apple Silicon (M1, M2, ...): `Libation.9.4.2-macOS-chardonnay-`**arm64**`.tgz`
- Apple Silicon (M1, M2, ...): `Libation.x.x.x-macOS-chardonnay-`**arm64**`.tgz`
- Intel: `Libation.x.x.x-macOS-chardonnay-`**x64**`.tgz`
- Move the extracted Libation app bundle to your applications folder.
- Open a terminal (Go > Utilities > Terminal)
- Copy/paste/run the following command (you'll be prompted to enter your password)
- Right-click on Libation and then click on open
- The first time, it will not immediately show you an option to open it. Just dismiss the dialog and do the same thing again (right-click -> open) then you will get an option to run the unsigned application. This takes about 10 seconds.
## If this doesn't work
You can add Libation as a safe app without touching Gatekeeper.
- Copy/paste/run the following command. Adjust the file path to the Libation.app on your computer if necessary.
```Console
xattr -r -d com.apple.quarantine ~/Downloads/Libation.app
```
- Close the terminal and use Libation!
## If this still doesn't work
- Copy/paste/run the following command (you'll be prompted to enter your Mac password)
```Console
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
```
- Close the terminal and use Libation!
```
* Close the terminal and use Libation!
## "Apple can't check app for malicious software"
From: [How to Open Anyway](https://support.apple.com/guide/mac-help/apple-cant-check-app-for-malicious-software-mchleab3a043/mac):
* On your Mac, choose Apple menu > System Settings, then click Privacy & Security in the sidebar. (You may need to scroll down.)
* Go to Security, then click Open.
* Click Open Anyway. This button is available for about an hour after you try to open the app.
* Enter your login password, then click OK.
## Troubleshooting
@@ -41,7 +67,7 @@ Libation comes with a recovery app called Hangover. You can start it by running
open /Applications/Libation.app --args hangover
```
## Runnign LibationCli
## Running LibationCli
Libation comes with a command-line interface. Unfortunately, due to the way apps are sandboxed on mac, its use is somewhat limited. To open a new sandboxed terminal in LibationCli's directory, run the following command:
```Console

View File

@@ -1,3 +1,10 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
# Naming Templates
File and Folder names can be customized using Libation's built-in tag template naming engine. To edit how folder and file names are created, go to Settings \> Download/Decrypt and edit the naming templates. If you're splitting your audiobook into multiple files by chapter, you can also use a custom template to set each chapter's title metadata tag by editing the template in Settings \> Audio File Options.

View File

@@ -1,6 +1,6 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.

View File

@@ -2,9 +2,11 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
# Table of Contents
- [Audible audiobook manager](#audible-audiobook-manager)

View File

@@ -86,8 +86,12 @@ delfiles=( 'libmp3lame.arm64.so' 'libmp3lame.x64.so' 'libmp3lame.x64.dll' 'libmp
if [[ "$ARCH" == "arm64" ]]
then
delfiles+=('libmp3lame.x64.dylib' 'ffmpegaac.x64.dylib')
mv $BUNDLE_MACOS/ffmpegaac.arm64.dylib $BUNDLE_MACOS/ffmpegaac.dylib
mv $BUNDLE_MACOS/libmp3lame.arm64.dylib $BUNDLE_MACOS/libmp3lame.dylib
else
delfiles+=('libmp3lame.arm64.dylib' 'ffmpegaac.arm64.dylib')
mv $BUNDLE_MACOS/ffmpegaac.x64.dylib $BUNDLE_MACOS/ffmpegaac.dylib
mv $BUNDLE_MACOS/libmp3lame.x64.dylib $BUNDLE_MACOS/libmp3lame.dylib
fi
@@ -111,4 +115,4 @@ mv $APP_FILE ./bundle/$APP_FILE
rm -r $BUNDLE
echo "Done!"
echo "Done!"

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="1.1.3" />
<PackageReference Include="AAXClean.Codecs" Version="1.1.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,11 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>11.3.2.1</Version>
<TargetFramework>net9.0</TargetFramework>
<Version>12.0.2.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="9.1.0" />
<PackageReference Include="Octokit" Version="14.0.0" />
<!-- Do not remove unused Serilog.Sinks -->
<!-- Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use. -->
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.ZipFile" Version="3.1.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,21 +1,22 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using ApplicationServices;
using ApplicationServices;
using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.IO;
using Dinah.Core.Logging;
using LibationFileManager;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
namespace AppScaffolding
{
public enum ReleaseIdentifier
public enum ReleaseIdentifier
{
None,
WindowsClassic = OS.Windows | Variety.Classic | Architecture.X64,
@@ -87,6 +88,9 @@ namespace AppScaffolding
//
Migrations.migrate_to_v6_6_9(config);
Migrations.migrate_to_v11_5_0(config);
Migrations.migrate_to_v11_6_5(config);
Migrations.migrate_to_v12_0_1(config);
}
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
@@ -124,6 +128,9 @@ namespace AppScaffolding
{ "MinimumLevel", "Information" },
{ "WriteTo", new JArray
{
// ABOUT SINKS
// Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use.
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
new JObject
{
@@ -227,12 +234,20 @@ namespace AppScaffolding
// begin logging session with a form feed
Log.Logger.Information("\r\n\f");
Log.Logger.Information("Begin. {@DebugInfo}", new
static int fileCount(FileManager.LongPath longPath)
{
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
catch { return -1; }
}
Log.Logger.Information("Begin. {@DebugInfo}", new
{
AppName = EntryAssembly.GetName().Name,
Version = BuildVersion.ToString(),
ReleaseIdentifier,
Configuration.OS,
Environment.OSVersion,
InteropFactory.InteropFunctionsType,
Mode = mode,
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
@@ -242,6 +257,7 @@ namespace AppScaffolding
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
config.AutoScan,
config.BetaOptIn,
config.UseCoverAsFolderIcon,
config.LibationFiles,
@@ -250,10 +266,12 @@ namespace AppScaffolding
config.InProgress,
AudibleFileStorage.DownloadsInProgressDirectory,
DownloadsInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
DownloadsInProgressFiles = fileCount(AudibleFileStorage.DownloadsInProgressDirectory),
AudibleFileStorage.DecryptInProgressDirectory,
DecryptInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
DecryptInProgressFiles = fileCount(AudibleFileStorage.DecryptInProgressDirectory),
disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value),
});
if (InteropFactory.InteropFunctionsType is null)
@@ -262,8 +280,11 @@ namespace AppScaffolding
private static void wireUpSystemEvents(Configuration configuration)
{
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
LibraryCommands.LibrarySizeChanged += (object _, List<DataLayer.LibraryBook> libraryBooks)
=> SearchEngineCommands.FullReIndex(libraryBooks);
LibraryCommands.BookUserDefinedItemCommitted += (_, books)
=> SearchEngineCommands.UpdateBooks(books);
}
public static UpgradeProperties GetLatestRelease()
@@ -390,5 +411,140 @@ namespace AppScaffolding
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
}
}
}
class FilterState_6_6_9
{
public bool UseDefault { get; set; }
public List<string> Filters { get; set; } = new();
}
public static void migrate_to_v12_0_1(Configuration config)
{
#nullable enable
//Migrate from version 1 file cache to the dictionary-based version 2 cache
const string FILENAME_V1 = "FileLocations.json";
const string FILENAME_V2 = "FileLocationsV2.json";
var jsonFileV1 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V1);
var jsonFileV2 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
if (!File.Exists(jsonFileV2) && File.Exists(jsonFileV1))
{
try
{
//FilePathCache loads the cache in its static constructor,
//so perform migration without using FilePathCache.CacheEntry
if (JArray.Parse(File.ReadAllText(jsonFileV1)) is not JArray v1Cache || v1Cache.Count == 0)
return;
Dictionary<string, JArray> cache = new();
//Convert to c# objects to speed up searching by ID inside the iterator
var allItems
= v1Cache
.Select(i => new
{
Id = i["Id"]?.Value<string>(),
Path = i["Path"]?["Path"]?.Value<string>()
}).Where(i => i.Id != null)
.ToArray();
foreach (var id in allItems.Select(i => i.Id).OfType<string>().Distinct())
{
//Use this opportunity to purge non-existent files and re-classify file types
//(due to *.aax files previously not being classified as FileType.AAXC)
var items = allItems
.Where(i => i.Id == id && File.Exists(i.Path))
.Select(i => new JObject
{
{ "Id", i.Id },
{ "FileType", (int)FileTypes.GetFileTypeFromPath(i.Path) },
{ "Path", new JObject{ { "Path", i.Path } } }
})
.ToArray();
if (items.Length == 0)
continue;
cache[id] = new JArray(items);
}
var cacheJson = new JObject { { "Dictionary", JObject.FromObject(cache) } };
var cacheFileText = cacheJson.ToString(Formatting.Indented);
void migrate()
{
File.WriteAllText(jsonFileV2, cacheFileText);
File.Delete(jsonFileV1);
}
try { migrate(); }
catch (IOException)
{
try { migrate(); }
catch (IOException)
{
migrate();
}
}
}
catch { /* eat */ }
}
#nullable restore
}
public static void migrate_to_v11_6_5(Configuration config)
{
//Settings migration for unsupported sample rates (#1116)
if (config.MaxSampleRate < AAXClean.SampleRate.Hz_8000)
config.MaxSampleRate = AAXClean.SampleRate.Hz_8000;
else if (config.MaxSampleRate > AAXClean.SampleRate.Hz_48000)
config.MaxSampleRate = AAXClean.SampleRate.Hz_48000;
}
public static void migrate_to_v11_5_0(Configuration config)
{
// Read file, but convert old format to new (with Name field) as necessary.
if (!File.Exists(QuickFilters.JsonFile))
{
QuickFilters.InMemoryState = new();
return;
}
try
{
if (JsonConvert.DeserializeObject<QuickFilters.FilterState>(File.ReadAllText(QuickFilters.JsonFile))
is QuickFilters.FilterState inMemState)
{
QuickFilters.InMemoryState = inMemState;
return;
}
}
catch
{
// Eat
}
try
{
if (JsonConvert.DeserializeObject<FilterState_6_6_9>(File.ReadAllText(QuickFilters.JsonFile))
is FilterState_6_6_9 inMemState)
{
// Copy old structure to new.
QuickFilters.InMemoryState = new();
QuickFilters.InMemoryState.UseDefault = inMemState.UseDefault;
foreach (var oldFilter in inMemState.Filters)
QuickFilters.InMemoryState.Filters.Add(new QuickFilters.NamedFilter(oldFilter, null));
return;
}
Debug.Assert(false, "Should not get here, QuickFilters.json deserialization issue");
}
catch
{
// Eat
}
}
}
}

View File

@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="NPOI" Version="2.6.2" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="NPOI" Version="2.7.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -126,7 +126,8 @@ namespace ApplicationServices
| 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.PdfUrl | LibraryOptions.ResponseGroupOptions.OriginAsin
| LibraryOptions.ResponseGroupOptions.IsFinished,
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
};
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
@@ -221,7 +222,7 @@ namespace ApplicationServices
{
int qtyChanged = await Task.Run(() => SaveContext(context));
if (qtyChanged > 0)
await Task.Run(finalizeLibrarySizeChange);
await Task.Run(() => finalizeLibrarySizeChange(context));
return qtyChanged;
}
catch (Exception ex)
@@ -328,7 +329,7 @@ namespace ApplicationServices
// this is any changes at all to the database, not just new books
if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange());
await Task.Run(() => finalizeLibrarySizeChange(context));
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
return newCount;
@@ -368,16 +369,16 @@ namespace ApplicationServices
using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges()
foreach (var lb in removeLibraryBooks)
// Entry() NoTracking entities before SaveChanges()
foreach (var lb in removeLibraryBooks)
{
lb.IsDeleted = true;
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
lb.IsDeleted = true;
context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
finalizeLibrarySizeChange();
finalizeLibrarySizeChange(context);
return qtyChanges;
}
@@ -397,16 +398,16 @@ namespace ApplicationServices
using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges()
foreach (var lb in libraryBooks)
{
lb.IsDeleted = false;
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
// Entry() NoTracking entities before SaveChanges()
foreach (var lb in libraryBooks)
{
lb.IsDeleted = false;
context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
finalizeLibrarySizeChange();
finalizeLibrarySizeChange(context);
return qtyChanges;
}
@@ -431,7 +432,7 @@ namespace ApplicationServices
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
finalizeLibrarySizeChange();
finalizeLibrarySizeChange(context);
return qtyChanges;
}
@@ -444,10 +445,14 @@ namespace ApplicationServices
#endregion
// call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
private static void finalizeLibrarySizeChange(LibationContext context)
{
var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
LibrarySizeChanged?.Invoke(null, library);
}
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
public static event EventHandler LibrarySizeChanged;
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.
@@ -517,17 +522,18 @@ namespace ApplicationServices
if (libraryBooks is null || !libraryBooks.Any())
return 0;
foreach (var book in libraryBooks)
action?.Invoke(book.Book.UserDefinedItem);
using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges()
foreach (var book in libraryBooks)
// Entry() instead of Attach() due to possible stack overflow with large tables
foreach (var book in libraryBooks)
{
context.Attach(book.Book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
context.Attach(book.Book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
action?.Invoke(book.Book.UserDefinedItem);
var udiEntity = context.Entry(book.Book.UserDefinedItem);
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
@@ -598,7 +604,8 @@ namespace ApplicationServices
var results = libraryBooks
.AsParallel()
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
.WithoutParents()
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
.ToList();
var booksFullyBackedUp = results.Count(r => r.status == LiberatedStatus.Liberated);

View File

@@ -115,6 +115,9 @@ namespace ApplicationServices
[Name("LastDownloadedVersion")]
public string LastDownloadedVersion { get; set; }
[Name("IsFinished")]
public bool IsFinished { get; set; }
}
public static class LibToDtos
{
@@ -141,7 +144,7 @@ namespace ApplicationServices
PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.LowestCategoryNames().Any() ? a.Book.LowestCategoryNames().Aggregate((a, b) => $"{a}, {b}") : "",
CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()),
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
@@ -153,8 +156,8 @@ namespace ApplicationServices
Language = a.Book.Language,
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
}).ToList();
IsFinished = a.Book.UserDefinedItem.IsFinished
}).ToList();
}
public static class LibraryExporter
{
@@ -229,6 +232,7 @@ namespace ApplicationServices
nameof(ExportDto.Language),
nameof(ExportDto.LastDownloaded),
nameof(ExportDto.LastDownloadedVersion),
nameof(ExportDto.IsFinished)
};
var col = 0;
foreach (var c in columns)
@@ -306,6 +310,7 @@ namespace ApplicationServices
}
row.CreateCell(++col).SetCellValue(dto.LastDownloadedVersion);
row.CreateCell(++col).SetCellValue(dto.IsFinished);
rowIndex++;
}

View File

@@ -48,6 +48,8 @@ namespace ApplicationServices
}
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 =>
{
@@ -94,8 +96,11 @@ namespace ApplicationServices
private static void fullReIndex(SearchEngine engine)
{
var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library);
fullReIndex(engine, library);
}
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
=> engine.CreateNewIndex(libraryBooks);
#endregion
}
}

View File

@@ -90,8 +90,10 @@ namespace AudibleUtilities
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)));
//// unfortunately, a 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;
}

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="9.0.0.1" />
<PackageReference Include="AudibleApi" Version="9.3.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@@ -10,14 +10,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="8.0.0.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="8.0.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -191,7 +191,7 @@ namespace DataLayer
ArgumentValidator.EnsureNotNull(ladders, nameof(ladders));
//Replace all existing category ladders.
//Some books make have duplocate ladders
//Some books make have duplicate ladders
CategoriesLink.Clear();
CategoriesLink.UnionWith(ladders.Distinct().Select(l => new BookCategory(this, l)));
}

View File

@@ -195,7 +195,23 @@ namespace DataLayer
}
}
}
#endregion
#endregion
#region IsFinished
private bool _isFinished;
public bool IsFinished
{
get => _isFinished;
set
{
if (value != _isFinished)
{
_isFinished = value;
OnItemChanged(nameof(IsFinished));
}
}
}
#endregion
public override string ToString() => $"{Book} {Rating} {Tags}";
}

View File

@@ -6,6 +6,7 @@ namespace DataLayer
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
{
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString)
=> optionsBuilder.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}
}

View File

@@ -0,0 +1,468 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20240911114741_MyComment")]
partial class MyComment
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.Property<int>("_categoriesCategoryId")
.HasColumnType("INTEGER");
b.Property<int>("_categoryLaddersCategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
b.HasIndex("_categoryLaddersCategoryLadderId");
b.ToTable("CategoryCategoryLadder");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<string>("Language")
.HasColumnType("TEXT");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("PictureLarge")
.HasColumnType("TEXT");
b.Property<string>("Subtitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<long>("_audioFormat")
.HasColumnType("INTEGER");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("CategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("BookId", "CategoryLadderId");
b.HasIndex("BookId");
b.HasIndex("CategoryLadderId");
b.ToTable("BookCategory");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.ToTable("Categories");
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Property<int>("CategoryLadderId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.HasKey("CategoryLadderId");
b.ToTable("CategoryLadders");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<bool>("AbsentFromLastScan")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.HasKey("BookId");
b.ToTable("LibraryBooks");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Order")
.HasColumnType("TEXT");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.HasOne("DataLayer.Category", null)
.WithMany()
.HasForeignKey("_categoriesCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.CategoryLadder", null)
.WithMany()
.HasForeignKey("_categoryLaddersCategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");
b1.Property<bool>("IsFinished")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("LastDownloaded")
.HasColumnType("TEXT");
b1.Property<string>("LastDownloadedVersion")
.HasColumnType("TEXT");
b1.Property<int?>("PdfStatus")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem", (string)null);
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("CategoriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.CategoryLadder", "CategoryLadder")
.WithMany("BooksLink")
.HasForeignKey("CategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("CategoryLadder");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("CategoriesLink");
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DataLayer.Migrations
{
/// <inheritdoc />
public partial class MyComment : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsFinished",
table: "UserDefinedItem",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsFinished",
table: "UserDefinedItem");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace DataLayer.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
@@ -312,6 +312,9 @@ namespace DataLayer.Migrations
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");
b1.Property<bool>("IsFinished")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("LastDownloaded")
.HasColumnType("TEXT");

View File

@@ -44,7 +44,11 @@ namespace DataLayer
public static bool IsEpisodeParent(this Book book)
=> book.ContentType is ContentType.Parent;
public static bool HasLiberated(this Book book)
public static IEnumerable<LibraryBook> WithoutParents(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks.Where(lb => !lb.Book.IsEpisodeParent());
public static bool HasLiberated(this Book book)
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
}

View File

@@ -21,8 +21,8 @@ namespace DataLayer
.AsNoTrackingWithIdentityResolution()
.GetLibrary()
.AsEnumerable()
.Where(lb => !lb.Book.IsEpisodeParent() || includeParents)
.ToList();
.Where(c => !c.Book.IsEpisodeParent() || includeParents)
.ToList();
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
=> context
@@ -91,7 +91,7 @@ namespace DataLayer
}
#nullable disable
public static IEnumerable<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
public static List<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
=> bookList
.Where(
lb =>

View File

@@ -164,6 +164,9 @@ namespace DtoImporterService
if (item.PictureLarge is not null)
book.PictureLarge = item.PictureLarge;
if (item.IsFinished is not null)
book.UserDefinedItem.IsFinished = item.IsFinished.Value;
// 2023-02-01
// updateBook must update language on books which were imported before the migration which added language.
// Can eventually delete this

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -50,7 +50,7 @@ namespace FileManager
lock (fsCacheLocker)
{
fsCache.Clear();
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption));
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
}
}
@@ -59,7 +59,7 @@ namespace FileManager
Stop();
lock (fsCacheLocker)
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption));
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
@@ -152,11 +152,23 @@ namespace FileManager
if (Path.GetFileName(path).Contains("LibationContext.db") || !File.Exists(path) && !Directory.Exists(path))
return;
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));
AddUniqueFiles(SafestEnumerateFiles(path));
else
AddUniqueFile(path);
}
private IEnumerable<LongPath> SafestEnumerateFiles(string path)
{
try
{
return FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption);
}
catch
{
return [];
}
}
private void AddUniqueFiles(IEnumerable<LongPath> newFiles)
{
foreach (var file in newFiles)

View File

@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="8.0.0.1" />
<PackageReference Include="Polly" Version="8.2.1" />
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
<PackageReference Include="Polly" Version="8.5.2" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -88,7 +88,7 @@ namespace FileManager
Replacement.OtherQuote(""),
Replacement.OpenAngleBracket(""),
Replacement.CloseAngleBracket(""),
Replacement.Colon(""),
Replacement.Colon("_"),
Replacement.Asterisk("✱"),
Replacement.QuestionMark(""),
Replacement.Pipe("⏐"),

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
<TrimMode>copyused</TrimMode>
@@ -21,9 +21,13 @@
<ApplicationIcon>hangover.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
</ItemGroup>
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\bin\Avalonia\Debug</OutputPath>
@@ -67,13 +71,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
<PackageReference Include="Avalonia" Version="11.2.4" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.5" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AssemblyName>Hangover</AssemblyName>
<UseWindowsForms>true</UseWindowsForms>
@@ -15,6 +15,10 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
</ItemGroup>
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\classic</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LibationAvalonia"
xmlns:controls="using:LibationAvalonia.Controls"
xmlns:dialogs="using:LibationAvalonia.Dialogs"
x:Class="LibationAvalonia.App"
Name="Libation">
@@ -12,6 +13,10 @@
<Application.Resources>
<ResourceDictionary>
<ControlTheme x:Key="{x:Type TextBlock}" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="Transparent" />
</ControlTheme>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
@@ -77,6 +82,64 @@
<Style Selector="Button">
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style Selector="ScrollBar">
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
<Setter Property="AllowAutoHide" Value="false"/>
</Style>
<Style Selector="dialogs|DialogWindow">
<Style Selector="^[UseCustomTitleBar=false]">
<Setter Property="SystemDecorations" Value="Full"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Background="{DynamicResource SystemControlBackgroundAltHighBrush}" Content="{TemplateBinding Content}" />
</ControlTemplate>
</Setter>
</Style>
<Style Selector="^[UseCustomTitleBar=true]">
<Setter Property="SystemDecorations" Value="BorderOnly"/>
<Setter Property="Template">
<ControlTemplate>
<Panel Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<Grid RowDefinitions="30,*">
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
<Border.Styles>
<Style Selector="Button#DialogCloseButton">
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Red" />
</Style>
<Style Selector="^ Path">
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
</Style>
</Style>
<Style Selector="^:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Style>
</Border.Styles>
<Grid ColumnDefinitions="Auto,*,Auto">
<Path Name="DialogWindowTitleIcon" Margin="3,5,0,5" Fill="{DynamicResource IconFill}" Stretch="Uniform" Data="{StaticResource LibationGlassIcon}"/>
<TextBlock Name="DialogWindowTitleTextBlock" Margin="8,0,0,0" VerticalAlignment="Center" FontWeight="DemiBold" FontSize="12" Grid.Column="1" Text="{TemplateBinding Title}" />
<Button Name="DialogCloseButton" Grid.Column="2">
<Path Fill="{DynamicResource SystemControlBackgroundBaseLowBrush}" VerticalAlignment="Center" Stretch="Uniform" RenderTransform="{StaticResource Rotate45Transform}" Data="{StaticResource CancelButtonIcon}" />
</Button>
</Grid>
</Border>
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
</Grid>
</Panel>
</ControlTemplate>
</Setter>
</Style>
</Style>
</Application.Styles>
<NativeMenu.Menu>

View File

@@ -1,8 +1,6 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
@@ -14,14 +12,13 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using ReactiveUI;
using DataLayer;
using Avalonia.Threading;
namespace LibationAvalonia
{
public class App : Application
{
public static Window MainWindow { get; private set; }
public static MainWindow MainWindow { get; private set; }
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
@@ -47,7 +44,7 @@ namespace LibationAvalonia
if (!config.LibationSettingsAreValid)
{
var defaultLibationFilesDir = Configuration.UserProfile;
var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
// check for existing settings in default location
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
@@ -85,19 +82,15 @@ namespace LibationAvalonia
// - error message, Exit()
if (setupDialog.IsNewUser)
{
Configuration.SetLibationFiles(Configuration.UserProfile);
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
if (setupDialog.Config.LibationSettingsAreValid)
{
var theme
= setupDialog.SelectedTheme.Content is nameof(ThemeVariant.Dark)
? nameof(ThemeVariant.Dark)
: nameof(ThemeVariant.Light);
string theme = setupDialog.SelectedTheme.Content as string;
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
await RunMigrationsAsync(setupDialog.Config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
@@ -181,7 +174,7 @@ namespace LibationAvalonia
if (continueResult == DialogResult.Yes)
{
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
config.Books = Configuration.DefaultBooksDirectory;
if (config.LibationSettingsAreValid)
{
@@ -208,17 +201,29 @@ namespace LibationAvalonia
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
{
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is "Dark" ? ThemeVariant.Dark : ThemeVariant.Light;
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) switch
{
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
nameof(ThemeVariant.Light) => ThemeVariant.Light,
// "System"
_ => ThemeVariant.Default
};
//Reload colors for current theme
//Reload colors for current theme
LoadStyles();
var mainWindow = new MainWindow();
desktop.MainWindow = MainWindow = mainWindow;
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
mainWindow.Loaded += MainWindow_Loaded;
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
mainWindow.Show();
}
private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var library = await LibraryTask;
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
}
private static void LoadStyles()
{
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));

View File

@@ -91,6 +91,32 @@
S 192,128 147,147
</StreamGeometry>
<StreamGeometry x:Key="LibationGlassIcon">
M262,8
h-117
a 192,200 0 0 0 -36,82
a 222,334 41 0 0 138,236
v158
h-81
a 16,16 0 0 0 0,32
h192
a 16 16 0 0 0 0,-32
h-81
v-158
a 222,334 -41 0 0 138,-236
a 192,200 0 0 0 -36,-82
h-117
m-99,30
a 192,200 0 0 0 -26,95
a 187.5,334 35 0 0 125,159
a 187.5,334 -35 0 0 125,-159
a 192,200 0 0 0 -26,-95
h-198
M158,136
a 168,305 35 0 0 104,136
a 168,305 -35 0 0 104,-136
</StreamGeometry>
</ResourceDictionary>
</Styles.Resources>
</Styles>

View File

@@ -107,7 +107,7 @@ namespace LibationAvalonia.Controls
if (known is Configuration.KnownDirectories.None)
{
directoryState.CustomDir = noSubDir;
directoryState.CustomDir = directory;
directoryState.CustomChecked = true;
}
else

View File

@@ -51,7 +51,9 @@ namespace LibationAvalonia.Controls
{
Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.ApplicationData,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyMusic,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles
};

View File

@@ -166,16 +166,7 @@
MinWidth="80"
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
ItemsSource="{CompiledBinding Themes}"/>
<TextBlock
Grid.Column="2"
FontSize="16"
FontWeight="Bold"
Margin="10,0"
VerticalAlignment="Center"
IsVisible="{CompiledBinding SelectionChanged}"
Text="Theme change takes effect on restart"/>
</Grid>
</Grid>
</UserControl>

View File

@@ -1,4 +1,6 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using LibationFileManager;
using System;
using System.Threading.Tasks;
@@ -9,19 +11,98 @@ namespace LibationAvalonia.Dialogs
{
public bool SaveAndRestorePosition { get; set; } = true;
public Control ControlToFocusOnShow { get; set; }
protected override Type StyleKeyOverride => typeof(DialogWindow);
public static readonly StyledProperty<bool> UseCustomTitleBarProperty =
AvaloniaProperty.Register<DialogWindow, bool>(nameof(UseCustomTitleBar));
public bool UseCustomTitleBar
{
get { return GetValue(UseCustomTitleBarProperty); }
set { SetValue(UseCustomTitleBarProperty, value); }
}
public DialogWindow()
{
this.HideMinMaxBtns();
this.KeyDown += DialogWindow_KeyDown;
this.Initialized += DialogWindow_Initialized;
this.Opened += DialogWindow_Opened;
this.Closing += DialogWindow_Closing;
KeyDown += DialogWindow_KeyDown;
Initialized += DialogWindow_Initialized;
Opened += DialogWindow_Opened;
Closing += DialogWindow_Closing;
UseCustomTitleBar = Configuration.IsWindows;
}
private bool fixedMinHeight = false;
private bool fixedMaxHeight = false;
private bool fixedHeight = false;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
const int customTitleBarHeight = 30;
if (UseCustomTitleBar)
{
if (change.Property == MinHeightProperty && !fixedMinHeight)
{
fixedMinHeight = true;
MinHeight += customTitleBarHeight;
fixedMinHeight = false;
}
if (change.Property == MaxHeightProperty && !fixedMaxHeight)
{
fixedMaxHeight = true;
MaxHeight += customTitleBarHeight;
fixedMaxHeight = false;
}
if (change.Property == HeightProperty && !fixedHeight)
{
fixedHeight = true;
Height += customTitleBarHeight;
fixedHeight = false;
}
}
base.OnPropertyChanged(change);
}
public DialogWindow(bool saveAndRestorePosition) : this()
{
SaveAndRestorePosition = saveAndRestorePosition;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (!UseCustomTitleBar)
return;
var closeButton = e.NameScope.Find<Button>("DialogCloseButton");
var border = e.NameScope.Get<Border>("DialogWindowTitleBorder");
var titleBlock = e.NameScope.Get<TextBlock>("DialogWindowTitleTextBlock");
var icon = e.NameScope.Get<Avalonia.Controls.Shapes.Path>("DialogWindowTitleIcon");
closeButton.Click += CloseButton_Click;
border.PointerPressed += Border_PointerPressed;
icon.IsVisible = Icon != null;
if (MinHeight == MaxHeight && MinWidth == MaxWidth)
{
CanResize = false;
border.Margin = new Thickness(0);
icon.Margin = new Thickness(8, 5, 0, 5);
}
}
private void Border_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
private void CloseButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
CancelAndClose();
}
private void DialogWindow_Initialized(object sender, EventArgs e)
{
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;

View File

@@ -2,11 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
Width="800" Height="450"
x:Class="LibationAvalonia.Dialogs.EditQuickFilters"
Title="Audible Accounts"
Icon="/Assets/libation.ico">
Icon="/Assets/libation.ico"
x:DataType="dialogs:EditQuickFilters">
<Grid RowDefinitions="*,Auto">
<Grid.Styles>
@@ -43,7 +45,14 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
IsReadOnly="False"
Binding="{Binding Name, Mode=TwoWay}"
Header="Name"/>
<DataGridTextColumn
Width="*"
IsReadOnly="False"

View File

@@ -12,8 +12,18 @@ namespace LibationAvalonia.Dialogs
public ObservableCollection<Filter> Filters { get; } = new();
public class Filter : ViewModels.ViewModelBase
{
private string _filterString;
{
private string _name;
public string Name
{
get => _name;
set
{
this.RaiseAndSetIfChanged(ref _name, value);
}
}
private string _filterString;
public string FilterString
{
get => _filterString;
@@ -25,6 +35,9 @@ namespace LibationAvalonia.Dialogs
}
}
public bool IsDefault { get; private set; } = true;
public QuickFilters.NamedFilter AsNamedFilter() => new(FilterString, Name);
}
public EditQuickFilters()
{
@@ -40,7 +53,7 @@ namespace LibationAvalonia.Dialogs
ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn));
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f }).ToList();
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f.Filter, Name = f.Name }).ToList();
allFilters.Add(new Filter());
foreach (var f in allFilters)
@@ -61,7 +74,7 @@ namespace LibationAvalonia.Dialogs
protected override void SaveAndClose()
{
QuickFilters.ReplaceAll(Filters.Where(f => !f.IsDefault).Select(f => f.FilterString));
QuickFilters.ReplaceAll(Filters.Where(f => !f.IsDefault).Select(x => x.AsNamedFilter()));
base.SaveAndClose();
}

View File

@@ -25,7 +25,6 @@ namespace LibationAvalonia.Dialogs
{
InitializeComponent();
this.HideMinMaxBtns();
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
LoadAccounts();

View File

@@ -12,8 +12,6 @@ namespace LibationAvalonia.Dialogs
{
InitializeComponent();
this.HideMinMaxBtns();
StringFields = @"
Search for wizard of oz:
title:oz

View File

@@ -2,12 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="350"
x:Class="LibationAvalonia.Dialogs.SetupDialog"
WindowStartupLocation="CenterScreen"
Width="500" Height="350"
Icon="/Assets/libation.ico"
Title="Welcome to Libation">
Title="Welcome to Libation"
x:DataType="dialogs:SetupDialog">
<Grid
Margin="10"
@@ -58,6 +60,7 @@
SelectedIndex="0"
SelectedItem="{Binding SelectedTheme, Mode=OneWayToSource}">
<ComboBox.Items>
<ComboBoxItem Content="System" />
<ComboBoxItem Content="Light" />
<ComboBoxItem Content="Dark" />
</ComboBox.Items>

View File

@@ -109,23 +109,5 @@ namespace LibationAvalonia
public int Width;
public bool IsMaximized;
}
public static void HideMinMaxBtns(this Window form)
{
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
return;
var currentStyle = GetWindowLong(handle.Handle, GWL_STYLE);
SetWindowLong(handle.Handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
}
const long WS_MINIMIZEBOX = 0x00020000L;
const long WS_MAXIMIZEBOX = 0x10000L;
const int GWL_STYLE = -16;
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLong")]
static extern long GetWindowLong(IntPtr hWnd, int nIndex);
[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, long dwNewLong);
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationIcon>Assets/libation.ico</ApplicationIcon>
@@ -14,6 +14,10 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
</ItemGroup>
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
@@ -70,13 +74,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.5" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.4" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.2.4" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.4" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -165,8 +165,6 @@ Libation.
var dialog = new MessageBoxWindow(saveAndRestorePosition);
dialog.HideMinMaxBtns();
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
dialog.DataContext = vm;
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
@@ -190,11 +188,13 @@ Libation.
tbx.Height = tbx.DesiredSize.Height;
tbx.Width = tbx.DesiredSize.Width;
dialog.MinHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
var absoluteHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
dialog.MinHeight = absoluteHeight;
dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width);
dialog.MaxHeight = dialog.MinHeight;
dialog.MaxHeight = absoluteHeight;
dialog.MaxWidth = dialog.MinWidth;
dialog.Height = dialog.MinHeight;
dialog.Height = absoluteHeight;
dialog.Width = dialog.MinWidth;
return dialog;
}

View File

@@ -46,11 +46,6 @@ namespace LibationAvalonia
try
{
var config = LibationScaffolding.RunPreConfigMigrations();
//Start as much work in parallel as possible.
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
var appBuilderTask = Task.Run(BuildAvaloniaApp);
if (config.LibationSettingsAreValid)
{
// most migrations go in here
@@ -62,9 +57,7 @@ namespace LibationAvalonia
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
}
appBuilderTask.GetAwaiter().GetResult().SetupWithLifetime(classicLifetimeTask.GetAwaiter().GetResult());
classicLifetimeTask.Result.Start(null);
BuildAvaloniaApp().StartWithClassicDesktopLifetime(null);
}
catch (Exception ex)
{

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -4,7 +4,6 @@ using DataLayer;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels
@@ -44,16 +43,15 @@ namespace LibationAvalonia.ViewModels
private void Configure_BackupCounts()
{
MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent()));
LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts();
LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts();
//Pass null to the setup count to get the whole library.
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
=> await SetBackupCountsAsync(null);
}
private async void setBackupCounts(IEnumerable<LibraryBook> libraryBooks = null)
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook> libraryBooks)
{
if (updateCountsTask?.IsCompleted ?? true)
{
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
var stats = await updateCountsTask;
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);

View File

@@ -13,12 +13,12 @@ namespace LibationAvalonia.ViewModels
{
partial class MainVM
{
private string lastGoodFilter = "";
private string _filterString;
private QuickFilters.NamedFilter lastGoodFilter = new(string.Empty, null);
private QuickFilters.NamedFilter _selectedNamedFilter = new(string.Empty, null);
private bool _firstFilterIsDefault = true;
/// <summary> Library filterting query </summary>
public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); }
public QuickFilters.NamedFilter SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
/// <summary> Indicates if the first quick filter is the default filter </summary>
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
@@ -50,19 +50,19 @@ namespace LibationAvalonia.ViewModels
QuickFilterMenuItems.Add(new Separator());
}
public void AddQuickFilterBtn() => QuickFilters.Add(FilterString);
public async Task FilterBtn() => await PerformFilter(FilterString);
public void AddQuickFilterBtn() => QuickFilters.Add(SelectedNamedFilter);
public async Task FilterBtn() => await PerformFilter(SelectedNamedFilter);
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
public async Task PerformFilter(string filterString)
public async Task PerformFilter(QuickFilters.NamedFilter namedFilter)
{
FilterString = filterString;
SelectedNamedFilter = namedFilter;
try
{
await ProductsDisplay.Filter(filterString);
lastGoodFilter = filterString;
await ProductsDisplay.Filter(namedFilter.Filter);
lastGoodFilter = namedFilter;
}
catch (Exception ex)
{
@@ -99,8 +99,8 @@ namespace LibationAvalonia.ViewModels
{
var command = ReactiveCommand.Create(async () => await PerformFilter(filter));
var menuItem = new MenuItem { Header = $"{++index}: {filter}", Command = command };
var nativeMenuItem = new NativeMenuItem { Header = $"{index}: {filter}", Command = command };
var menuItem = new MenuItem { Header = $"{++index}: {(string.IsNullOrWhiteSpace(filter.Name) ? filter.Filter : filter.Name)}", Command = command };
var nativeMenuItem = new NativeMenuItem { Header = $"{index}: {(string.IsNullOrWhiteSpace(filter.Name) ? filter.Filter : filter.Name)}", Command = command };
if (Configuration.IsMacOs && index <= 10)
{

View File

@@ -10,8 +10,8 @@ namespace LibationAvalonia.ViewModels
{
partial class MainVM
{
private int _visibleNotLiberated = 1;
private int _visibleCount = 1;
private int _visibleNotLiberated = 0;
private int _visibleCount = 0;
/// <summary> The Bottom-right visible book count status text </summary>
public string VisibleCountText => $"Visible: {_visibleCount}";

View File

@@ -1,7 +1,10 @@
using ApplicationServices;
using DataLayer;
using LibationAvalonia.Views;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels
{
@@ -20,7 +23,7 @@ namespace LibationAvalonia.ViewModels
MainWindow = mainWindow;
ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
LibraryCommands.LibrarySizeChanged += async (_, _) => await ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
LibraryCommands.LibrarySizeChanged += LibraryCommands_LibrarySizeChanged;
Configure_NonUI();
Configure_BackupCounts();
@@ -34,6 +37,13 @@ namespace LibationAvalonia.ViewModels
Configure_VisibleBooks();
}
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
{
await Task.WhenAll(
SetBackupCountsAsync(fullLibrary),
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
}
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
}
}

View File

@@ -118,7 +118,15 @@ namespace LibationAvalonia.ViewModels
#region Add Books to Queue
private bool isBookInQueue(LibraryBook libraryBook)
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
{
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
if (entry == null)
return false;
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
return !Queue.RemoveCompleted(entry);
else
return true;
}
public void AddDownloadPdf(LibraryBook libraryBook)
=> AddDownloadPdf(new List<LibraryBook>() { libraryBook });

View File

@@ -11,6 +11,7 @@ using ReactiveUI;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
@@ -28,7 +29,13 @@ namespace LibationAvalonia.ViewModels
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
private HashSet<IGridEntry> FilteredInGridEntries;
public string FilterString { get; private set; }
public DataGridCollectionView GridEntries { get; private set; }
private DataGridCollectionView _gridEntries;
public DataGridCollectionView GridEntries
{
get => _gridEntries;
private set => this.RaiseAndSetIfChanged(ref _gridEntries, value);
}
private bool _removeColumnVisible;
public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); }
@@ -96,16 +103,17 @@ namespace LibationAvalonia.ViewModels
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
{
GridEntries = new(SOURCE) { Filter = CollectionFilter };
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
AvaloniaSynchronizationContext.SetSynchronizationContext(sc);
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
//Create the filtered-in list before adding entries to avoid a refresh
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
//Adding entries to the Source list will invoke CollectionFilter
SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null)));
//Perform on UI thread for safety
await Dispatcher.UIThread.InvokeAsync(() => SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null))));
//Add all children beneath their parent
foreach (var series in seriesEntries)
@@ -115,6 +123,10 @@ namespace LibationAvalonia.ViewModels
SOURCE.Insert(++seriesIndex, child);
}
// Adding SOURCE to the DataGridViewCollection after building the source
//Saves ~500 ms on a library of ~4500 books.
//Perform on UI thread for safety
await Dispatcher.UIThread.InvokeAsync(() => GridEntries = new(SOURCE) { Filter = CollectionFilter });
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
GridEntries_CollectionChanged();
}

View File

@@ -25,7 +25,9 @@ namespace LibationAvalonia.ViewModels.Settings
public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; }
public AvaloniaList<EnumDiaplay<SampleRate>> SampleRates { get; }
= new(Enum.GetValues<SampleRate>().Select(v => new EnumDiaplay<SampleRate>(v, $"{(int)v} Hz")));
= new(Enum.GetValues<SampleRate>()
.Where(r => r >= SampleRate.Hz_8000 && r <= SampleRate.Hz_48000)
.Select(v => new EnumDiaplay<SampleRate>(v, $"{(int)v} Hz")));
public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; }
= new(

View File

@@ -21,6 +21,7 @@ namespace LibationAvalonia.ViewModels.Settings
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
{
Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.ApplicationData,
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs,

View File

@@ -31,10 +31,10 @@ namespace LibationAvalonia.ViewModels.Settings
LoggingLevel = config.LogLevel;
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
ThemeVariant = initialThemeVariant
= Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is nameof(Avalonia.Styling.ThemeVariant.Dark)
? nameof(Avalonia.Styling.ThemeVariant.Dark)
: nameof(Avalonia.Styling.ThemeVariant.Light);
ThemeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant));
if (string.IsNullOrWhiteSpace(initialThemeVariant))
ThemeVariant = initialThemeVariant = "System";
}
public void SaveSettings(Configuration config)
@@ -67,7 +67,8 @@ namespace LibationAvalonia.ViewModels.Settings
{
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.MyMusic,
};
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
@@ -83,7 +84,7 @@ namespace LibationAvalonia.ViewModels.Settings
public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor));
public string GridFontScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridFontScaleFactor));
public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
public string[] Themes { get; } = { nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
public string[] Themes { get; } = { "System", nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
public string BooksDirectory { get; set; }
public bool SavePodcastsToParentFolder { get; set; }
@@ -100,11 +101,14 @@ namespace LibationAvalonia.ViewModels.Settings
set
{
this.RaiseAndSetIfChanged(ref themeVariant, value);
SelectionChanged = ThemeVariant != initialThemeVariant;
this.RaisePropertyChanged(nameof(SelectionChanged));
App.Current.RequestedThemeVariant = themeVariant switch
{
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
// "System"
_ => Avalonia.Styling.ThemeVariant.Default
};
}
}
public bool SelectionChanged { get; private set; }
}
}

View File

@@ -191,7 +191,7 @@
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
</StackPanel>
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding SelectedNamedFilter.Filter, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>

View File

@@ -8,12 +8,12 @@ using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
public partial class MainWindow : ReactiveWindow<MainVM>
{
public event EventHandler<List<LibraryBook>> LibraryLoaded;
public MainWindow()
{
DataContext = new MainVM(this);
@@ -23,7 +23,6 @@ namespace LibationAvalonia.Views
Opened += MainWindow_Opened;
Closing += MainWindow_Closing;
LibraryLoaded += MainWindow_LibraryLoaded;
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) });
@@ -56,21 +55,24 @@ namespace LibationAvalonia.Views
this.SaveSizeAndLocation(Configuration.Instance);
}
private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
{
if (QuickFilters.UseDefault)
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
await ViewModel.ProductsDisplay.BindToGridAsync(dbBooks);
}
private void selectAndFocusSearchBox()
{
filterSearchTb.SelectAll();
filterSearchTb.Focus();
}
public void OnLibraryLoaded(List<LibraryBook> initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary);
public async Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary)
{
//Get the ViewModel before crossing the await boundary
var vm = ViewModel;
if (QuickFilters.UseDefault)
await vm.PerformFilter(QuickFilters.Filters.FirstOrDefault());
await Task.WhenAll(
vm.SetBackupCountsAsync(initialLibrary),
Task.Run(() => vm.ProductsDisplay.BindToGridAsync(initialLibrary)));
}
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
@@ -79,7 +81,7 @@ namespace LibationAvalonia.Views
{
if (e.Key == Key.Return)
{
await ViewModel.PerformFilter(ViewModel.FilterString);
await ViewModel.PerformFilter(ViewModel.SelectedNamedFilter);
// silence the 'ding'
e.Handled = true;

View File

@@ -6,7 +6,7 @@
xmlns:views="clr-namespace:LibationAvalonia.Views"
xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="850"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="650"
x:Class="LibationAvalonia.Views.ProcessQueueControl">
<UserControl.Resources>
@@ -39,7 +39,8 @@
<ScrollViewer
Name="scroller"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
VerticalScrollBarVisibility="Auto"
AllowAutoHide="False">
<ItemsRepeater IsVisible="True"
Grid.Column="0"
Name="repeater"

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@@ -11,6 +11,10 @@
<IsPublishable>True</IsPublishable>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
</ItemGroup>
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -25,9 +25,18 @@ namespace LibationFileManager
static AudibleFileStorage()
{
//Clean up any partially-decrypted files from previous Libation instances.
//Do no clean DownloadsInProgressDirectory because those files are resumable
foreach (var tempFile in Directory.EnumerateFiles(DecryptInProgressDirectory))
FileUtility.SaferDelete(tempFile);
//Do not clean DownloadsInProgressDirectory. Those files are resumable.
try
{
foreach (var tempFile in FileUtility.SaferEnumerateFiles(DecryptInProgressDirectory))
FileUtility.SaferDelete(tempFile);
}
catch (Exception ex)
{
// since this is a static constructor which may be called very early, do not assume Serilog is initialized
try { Serilog.Log.Error(ex, "Error cleaning up partially-decrypted files"); }
catch { }
}
}
@@ -41,7 +50,7 @@ namespace LibationFileManager
get
{
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
Configuration.Instance.Books = Path.Combine(Configuration.UserProfile, "Books");
Configuration.Instance.Books = Configuration.DefaultBooksDirectory;
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
}
}

View File

@@ -14,8 +14,12 @@ namespace LibationFileManager
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation"));
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
public static string LocalAppData => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation"));
public static string DefaultLibationFilesDirectory => !IsWindows ? LocalAppData : UserProfile;
public static string DefaultBooksDirectory => Path.Combine(!IsWindows ? MyMusic : UserProfile, nameof(Books));
public enum KnownDirectories
{
@@ -34,19 +38,27 @@ namespace LibationFileManager
MyDocs = 4,
[Description("Your settings folder (aka: Libation Files)")]
LibationFiles = 5
}
// use func calls so we always get the latest value of LibationFiles
private static List<(KnownDirectories directory, Func<string?> getPathFunc)> directoryOptionsPaths { get; } = new()
LibationFiles = 5,
[Description("User Application Data Folder")]
ApplicationData = 6,
[Description("My Music")]
MyMusic = 7,
}
// use func calls so we always get the latest value of LibationFiles
private static List<(KnownDirectories directory, Func<string?> getPathFunc)> directoryOptionsPaths { get; } = new()
{
(KnownDirectories.None, () => null),
(KnownDirectories.ApplicationData, () => LocalAppData),
(KnownDirectories.MyMusic, () => MyMusic),
(KnownDirectories.UserProfile, () => UserProfile),
(KnownDirectories.AppDir, () => AppDir_Relative),
(KnownDirectories.WinTemp, () => WinTemp),
(KnownDirectories.MyDocs, () => MyDocs),
// this is important to not let very early calls try to accidentally load LibationFiles too early.
// also, keep this at bottom of this list
(KnownDirectories.LibationFiles, () => libationFilesPathCache)
(KnownDirectories.LibationFiles, () => LibationSettingsDirectory)
};
public static string? GetKnownDirectoryPath(KnownDirectories directory)
{

View File

@@ -22,11 +22,11 @@ namespace LibationFileManager
{
get
{
if (libationFilesPathCache is not null)
return libationFilesPathCache;
if (LibationSettingsDirectory is not null)
return LibationSettingsDirectory;
// FIRST: must write here before SettingsFilePath in next step reads cache
libationFilesPathCache = getLibationFilesSettingFromJson();
LibationSettingsDirectory = getLibationFilesSettingFromJson();
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
persistentDictionary = new PersistentDictionary(SettingsFilePath);
@@ -42,11 +42,14 @@ namespace LibationFileManager
SetWithJsonPath(jsonpath, "path", logPath, true);
return libationFilesPathCache;
return LibationSettingsDirectory;
}
}
private static string? libationFilesPathCache { get; set; }
/// <summary>
/// Directory pointed to by appsettings.json
/// </summary>
private static string? LibationSettingsDirectory { get; set; }
/// <summary>
/// Try to find appsettings.json in the following locations:
@@ -79,7 +82,7 @@ namespace LibationFileManager
string[] possibleAppsettingsDirectories = new[]
{
ProcessDirectory,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation"),
LocalAppData,
UserProfile,
Path.Combine(Path.GetTempPath(), "Libation")
};
@@ -106,9 +109,15 @@ namespace LibationFileManager
}
//Valid appsettings.json not found. Try to create it in each folder.
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile } }.ToString(Formatting.Indented);
var endingContents = new JObject { { LIBATION_FILES_KEY, DefaultLibationFilesDirectory } }.ToString(Formatting.Indented);
foreach (var dir in possibleAppsettingsDirectories)
{
//Don't try to create appsettings.json in the program files directory on *.nix systems.
//However, still _look_ for one there for backwards compatibility with previous installations
if (!IsWindows && dir == ProcessDirectory)
continue;
var appsettingsFile = Path.Combine(dir, appsettings_filename);
try
@@ -180,7 +189,7 @@ namespace LibationFileManager
public static void SetLibationFiles(string directory)
{
libationFilesPathCache = null;
LibationSettingsDirectory = null;
var startingContents = File.ReadAllText(AppsettingsJsonFile);
var jObj = JObject.Parse(startingContents);

View File

@@ -321,7 +321,7 @@ namespace LibationFileManager
set => setTemplate<Templates.ChapterFileTemplate>(value);
}
[Description("How to format the file's Tile stored in metadata")]
[Description("How to format the file's Title stored in metadata")]
public string ChapterTitleTemplate
{
get => getTemplate<Templates.ChapterTitleTemplate>();

View File

@@ -18,9 +18,8 @@ namespace LibationFileManager
var pDic = new PersistentDictionary(settingsFile, isReadOnly: false);
var booksDir = pDic.GetString(nameof(Books));
if (booksDir is null) return false;
if (pDic.GetString(nameof(Books)) is not string booksDir)
return false;
if (!Directory.Exists(booksDir))
{
@@ -28,17 +27,21 @@ namespace LibationFileManager
throw new DirectoryNotFoundException(settingsFile);
//"Books" is not null, so setup has already been run.
//Since Books can't be found, try to create it in Libation settings folder
booksDir = Path.Combine(dir, nameof(Books));
try
//Since Books can't be found, try to create it
//and then revert to the default books directory
foreach (string d in new string[] { booksDir, DefaultBooksDirectory })
{
Directory.CreateDirectory(booksDir);
try
{
Directory.CreateDirectory(d);
pDic.SetString(nameof(Books), booksDir);
pDic.SetString(nameof(Books), d);
return booksDir is not null && Directory.Exists(booksDir);
return Directory.Exists(d);
}
catch { /* Do Nothing */ }
}
catch { return false; }
return false;
}
return true;

View File

@@ -1,8 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core.Collections.Immutable;
using FileManager;
using Newtonsoft.Json;
@@ -13,78 +13,98 @@ namespace LibationFileManager
{
public record CacheEntry(string Id, FileType FileType, LongPath Path);
private const string FILENAME = "FileLocations.json";
private const string FILENAME_V2 = "FileLocationsV2.json";
public static event EventHandler<CacheEntry>? Inserted;
public static event EventHandler<CacheEntry>? Removed;
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
private static LongPath jsonFileV2 => Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
private static LongPath jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME);
private static readonly FileCacheV2<CacheEntry> Cache = new();
static FilePathCache()
{
static FilePathCache()
{
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
if (!File.Exists(jsonFile))
if (!File.Exists(jsonFileV2))
return;
try
{
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(jsonFile));
if (list is null)
throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy.");
cache = new Cache<CacheEntry>(list);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFile });
lock (locker)
File.Delete(jsonFile);
return;
}
}
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
=> getEntries(entry => entry.Id == id)
.Select(entry => (entry.FileType, entry.Path))
.ToList();
public static LongPath? GetFirstPath(string id, FileType type)
=> getEntries(entry => entry.Id == id && entry.FileType == type)
?.FirstOrDefault()
?.Path;
private static IEnumerable<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
{
var entries = cache.Where(predicate).ToList();
if (entries is null || !entries.Any())
return Enumerable.Empty<CacheEntry>();
remove(entries.Where(e => !File.Exists(e.Path)).ToList());
return cache.Where(predicate).ToList();
}
private static void remove(List<CacheEntry> entries)
{
if (entries is null)
return;
lock (locker)
try
{
foreach (var entry in entries)
{
cache.Remove(entry);
Removed?.Invoke(null, entry);
}
save();
Cache = JsonConvert.DeserializeObject<FileCacheV2<CacheEntry>>(File.ReadAllText(jsonFileV2))
?? throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy.");
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFileV2 });
lock (locker)
File.Delete(jsonFileV2);
return;
}
}
public static void Insert(string id, string path)
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
{
var matchingFiles = Cache.GetIdEntries(id);
bool cacheChanged = false;
//Verify all entries exist
for (int i = 0; i < matchingFiles.Count; i++)
{
if (!File.Exists(matchingFiles[i].Path))
{
var entryToRemove = matchingFiles[i];
matchingFiles.RemoveAt(i);
cacheChanged |= Remove(entryToRemove);
}
}
if (cacheChanged)
save();
return matchingFiles.Select(e => (e.FileType, e.Path)).ToList();
}
public static LongPath? GetFirstPath(string id, FileType type)
{
var matchingFiles = Cache.GetIdEntries(id).Where(e => e.FileType == type).ToList();
bool cacheChanged = false;
try
{
//Verify entries exist, but return first matching 'type'
for (int i = 0; i < matchingFiles.Count; i++)
{
if (File.Exists(matchingFiles[i].Path))
return matchingFiles[i].Path;
else
{
var entryToRemove = matchingFiles[i];
matchingFiles.RemoveAt(i);
cacheChanged |= Remove(entryToRemove);
}
}
return null;
}
finally
{
if (cacheChanged)
save();
}
}
private static bool Remove(CacheEntry entry)
{
if (Cache.Remove(entry.Id, entry))
{
Removed?.Invoke(null, entry);
return true;
}
return false;
}
public static void Insert(string id, string path)
{
var type = FileTypes.GetFileTypeFromPath(path);
Insert(new CacheEntry(id, type, path));
@@ -92,7 +112,7 @@ namespace LibationFileManager
public static void Insert(CacheEntry entry)
{
cache.Add(entry);
Cache.Add(entry.Id, entry);
Inserted?.Invoke(null, entry);
save();
}
@@ -102,7 +122,7 @@ namespace LibationFileManager
private static void save()
{
// create json if not exists
static void resave() => File.WriteAllText(jsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented));
static void resave() => File.WriteAllText(jsonFileV2, JsonConvert.SerializeObject(Cache, Formatting.Indented));
lock (locker)
{
@@ -112,11 +132,56 @@ namespace LibationFileManager
try { resave(); }
catch (IOException ex)
{
Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME}");
Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME_V2}");
throw;
}
}
}
}
}
}
private class FileCacheV2<TEntry>
{
[JsonProperty]
private readonly ConcurrentDictionary<string, List<TEntry>> Dictionary = new();
private static object lockObject = new();
public List<TEntry> GetIdEntries(string id)
{
static List<TEntry> empty() => new();
return Dictionary.TryGetValue(id, out var entries) ? entries.ToList() : empty();
}
public void Add(string id, TEntry entry)
{
Dictionary.AddOrUpdate(id, [entry], (id, entries) => { entries.Add(entry); return entries; });
}
public void AddRange(string id, IEnumerable<TEntry> entries)
{
Dictionary.AddOrUpdate(id, entries.ToList(), (id, entries) =>
{
entries.AddRange(entries);
return entries;
});
}
public bool Remove(string id, TEntry entry)
{
lock (lockObject)
{
if (Dictionary.TryGetValue(id, out List<TEntry>? entries))
{
var removed = entries?.Remove(entry) ?? false;
if (removed && entries?.Count == 0)
{
Dictionary.Remove(id, out _);
}
return removed;
}
else return false;
}
}
}
}
}

View File

@@ -11,6 +11,7 @@ namespace LibationFileManager
{
private static Dictionary<string, FileType> dic => new()
{
["aax"] = FileType.AAXC,
["aaxc"] = FileType.AAXC,
["cue"] = FileType.Cue,
["pdf"] = FileType.PDF,

View File

@@ -95,9 +95,9 @@ namespace LibationFileManager
var assembly = CurrentDomain_AssemblyResolve_internal(asmName, here: here);
lowEffortCache[key] = assembly;
//Let the runtime handle any dll not found exceptions.
// Let the runtime handle any dll not found exceptions
if (assembly is null)
Serilog.Log.Logger.Error($"Unable to load module {args.Name}");
Serilog.Log.Logger.Warning($"Unable to load module {args.Name}");
return assembly;
}

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
<PackageReference Include="NameParserSharp" Version="1.5.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
</ItemGroup>

View File

@@ -1,9 +1,9 @@
using System;
using Dinah.Core.Collections.Generic;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core.Collections.Generic;
using Newtonsoft.Json;
#nullable enable
namespace LibationFileManager
@@ -14,32 +14,29 @@ namespace LibationFileManager
public static event EventHandler? UseDefaultChanged;
internal class FilterState
public class FilterState
{
public bool UseDefault { get; set; }
public List<string> Filters { get; set; } = new List<string>();
public List<NamedFilter> Filters { get; set; } = new();
}
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "QuickFilters.json");
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
static FilterState inMemoryState { get; }
= File.Exists(JsonFile) && JsonConvert.DeserializeObject<FilterState>(File.ReadAllText(JsonFile)) is FilterState inMemState
? inMemState
: new FilterState();
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
public static FilterState? InMemoryState { get; set; }
public static bool UseDefault
{
get => inMemoryState.UseDefault;
get => InMemoryState?.UseDefault ?? false;
set
{
if (UseDefault == value)
if (InMemoryState is null || UseDefault == value)
return;
lock (locker)
{
inMemoryState.UseDefault = value;
InMemoryState.UseDefault = value;
save(false);
}
@@ -47,68 +44,85 @@ namespace LibationFileManager
}
}
public static IEnumerable<string> Filters => inMemoryState.Filters.AsReadOnly();
public static void Add(string filter)
// Note that records overload equality automagically, so should be able to
// compare these the same way as comparing simple strings.
public record NamedFilter(string Filter, string Name)
{
if (string.IsNullOrWhiteSpace(filter))
public string Filter { get; set; } = Filter;
public string? Name { get; set; } = Name;
}
public static IEnumerable<NamedFilter> Filters
=> InMemoryState?.Filters.AsReadOnly() ?? Enumerable.Empty<NamedFilter>();
public static void Add(NamedFilter namedFilter)
{
if (namedFilter == null)
throw new ArgumentNullException(nameof(namedFilter));
if (string.IsNullOrWhiteSpace(namedFilter.Filter))
return;
filter = filter.Trim();
namedFilter.Filter = namedFilter.Filter?.Trim() ?? string.Empty;
namedFilter.Name = namedFilter.Name?.Trim() ?? null;
lock (locker)
{
// check for duplicate
if (inMemoryState.Filters.ContainsInsensative(filter))
lock (locker)
{
InMemoryState ??= new();
// check for duplicates
if (InMemoryState.Filters.Select(x => x.Filter).ContainsInsensative(namedFilter.Filter))
return;
inMemoryState.Filters.Add(filter);
InMemoryState.Filters.Add(namedFilter);
save();
}
}
public static void Remove(string filter)
public static void Remove(NamedFilter filter)
{
lock (locker)
{
inMemoryState.Filters.Remove(filter);
if (InMemoryState is null)
return;
InMemoryState.Filters.Remove(filter);
save();
}
}
public static void Edit(string oldFilter, string newFilter)
public static void Edit(NamedFilter oldFilter, NamedFilter newFilter)
{
lock (locker)
{
var index = inMemoryState.Filters.IndexOf(oldFilter);
if (index < 0)
if (InMemoryState is null || InMemoryState.Filters.IndexOf(oldFilter) < 0)
return;
inMemoryState.Filters = inMemoryState.Filters.Select(f => f == oldFilter ? newFilter : f).ToList();
InMemoryState.Filters = InMemoryState.Filters.Select(f => f == oldFilter ? newFilter : f).ToList();
save();
}
}
public static void ReplaceAll(IEnumerable<string> filters)
public static void ReplaceAll(IEnumerable<NamedFilter> filters)
{
filters = filters
.Where(f => !string.IsNullOrWhiteSpace(f))
.Distinct()
.Select(f => f.Trim());
.Where(f => !string.IsNullOrWhiteSpace(f.Filter))
.Distinct();
foreach (var filter in filters)
filter.Filter = filter.Filter.Trim();
lock (locker)
{
inMemoryState.Filters = new List<string>(filters);
InMemoryState ??= new();
InMemoryState.Filters = new List<NamedFilter>(filters);
save();
}
}
private static object locker { get; } = new object();
private static object locker { get; } = new();
// ONLY call this within lock()
private static void save(bool invokeUpdatedEvent = true)
{
// create json if not exists
void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(inMemoryState, Formatting.Indented));
void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(InMemoryState, Formatting.Indented));
try { resave(); }
catch (IOException)
{

View File

@@ -15,7 +15,7 @@ namespace LibationFileManager
try
{
//Currently only works for Windows and macOS
if (!Configuration.Instance.UseCoverAsFolderIcon || Configuration.IsLinux)
if (!Configuration.Instance.UseCoverAsFolderIcon)
return;
// get path of cover art in Images dir. Download first if not exists

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@@ -9,7 +9,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="LuceneNet303r2" Version="3.0.3.8" />
<PackageReference Include="LuceneNet303r2" Version="3.0.3.9" />
</ItemGroup>
<ItemGroup>

View File

@@ -54,6 +54,8 @@ namespace LibationSearchEngine
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.Error).ToString(), "LiberatedError" },
{ FieldType.Bool, lb => lb.Book.IsEpisodeChild().ToString(), "Podcast", "Podcasts", "IsPodcast", "Episode", "Episodes", "IsEpisode" },
{ FieldType.Bool, lb => lb.AbsentFromLastScan.ToString(), "AbsentFromLastScan", "Absent" },
{ FieldType.Bool, lb => (!string.IsNullOrWhiteSpace(lb.Book.SeriesNames())).ToString(), "IsInSeries", "InSeries" },
{ FieldType.Bool, lb => lb.Book.UserDefinedItem.IsFinished.ToString(), nameof(UserDefinedItem.IsFinished), "Finished", "IsFinished" },
// all numbers are padded to 8 char.s
// This will allow a single method to auto-pad numbers. The method will match these as well as date: yyyymmdd
{ FieldType.Number, lb => lb.Book.LengthInMinutes.ToLuceneString(), nameof(Book.LengthInMinutes), "Length", "Minutes" },

Some files were not shown because too many files have changed in this diff Show More