Compare commits

..

267 Commits

Author SHA1 Message Date
Robert McRackan
7d694229c1 Prep for version release 2022-06-08 14:48:44 -04:00
rmcrackan
cdb6c9a1a4 Merge pull request #268 from Mbucari/master
Address issues in 263
2022-06-08 14:46:17 -04:00
Michael Bucari-Tovo
cc1d2b423f Fix an oopsie 2022-06-08 12:15:21 -06:00
Michael Bucari-Tovo
508e031143 Move all event invocations outside locks 2022-06-08 12:08:15 -06:00
Michael Bucari-Tovo
5a093a9a04 add event keyword 2022-06-08 10:53:45 -06:00
Michael Bucari-Tovo
074d647d19 Improve Query 2022-06-08 10:36:06 -06:00
Michael Bucari-Tovo
6cb98f99c5 Use new content type queries 2022-06-08 10:34:05 -06:00
Michael Bucari-Tovo
7d28681b23 Move queries into DataLayer 2022-06-08 10:08:18 -06:00
Michael Bucari-Tovo
859a8e933c Formatting 2022-06-08 09:46:11 -06:00
Michael Bucari-Tovo
a476d5986d Update dependency 2022-06-08 09:44:06 -06:00
Michael Bucari-Tovo
31812bc2d9 Refactoring 2022-06-08 09:24:06 -06:00
Michael Bucari-Tovo
30ba69eca7 Minor refactoring. 2022-06-08 08:52:25 -06:00
Michael Bucari-Tovo
cf1bc1c252 By defauly, only get actual books and not parents from DB 2022-06-08 08:40:25 -06:00
Michael Bucari-Tovo
ee109ba67d Refactor 2022-06-08 08:39:59 -06:00
Michael Bucari-Tovo
9c6211e8e0 Improve UI speed when adding many books to queue at once. 2022-06-08 08:39:17 -06:00
Michael Bucari-Tovo
0729e4ab09 Minor refactor 2022-06-07 15:41:33 -06:00
Michael Bucari-Tovo
5cbe728631 Don't add series parents to list 2022-06-07 15:32:49 -06:00
Michael Bucari-Tovo
920f4df213 Use new ContentType.Parent to add series info to grid display 2022-06-07 15:28:16 -06:00
Michael Bucari-Tovo
c48eacd9af Add ContentType.Parent
Import Series parent when only individual episodes are in library
2022-06-07 15:27:18 -06:00
Michael Bucari-Tovo
30e6deeeaa Add migration to cleans DB of 7.10.1 hack 2022-06-07 15:25:52 -06:00
Robert McRackan
5bc76a3160 New debugging tool: "Hangover". Will be packaged with all releases 2022-06-01 11:49:30 -04:00
Robert McRackan
114925ebce Global exception handling. Threadsafe MessageBoxAlertAdminDialog 2022-05-27 13:38:28 -04:00
Robert McRackan
5a80a0cc06 Second bug fix for issue 263 2022-05-27 07:15:15 -04:00
rmcrackan
aebefac7e6 Merge pull request #266 from Mbucari/master
Fix my own screwup
2022-05-27 07:10:02 -04:00
Michael Bucari-Tovo
b2d0ee41f2 Fix my own screwup 2022-05-26 21:26:56 -06:00
Robert McRackan
9c20250b0a increm ver 2022-05-26 21:10:12 -04:00
rmcrackan
b196836fca Merge pull request #264 from Mbucari/master
Fix for episodes with no series link
2022-05-26 20:41:55 -04:00
Michael Bucari-Tovo
d9fbcc615a Change flow 2022-05-26 18:06:44 -06:00
Michael Bucari-Tovo
fb247fb33f Add better handling for parents and series with no children. 2022-05-26 17:29:55 -06:00
Michael Bucari-Tovo
61f4dbd896 No need to make a new list. 2022-05-26 16:50:43 -06:00
Michael Bucari-Tovo
2c86571818 Better identification of Chilv vs Parent from SeriesBook.Order 2022-05-26 16:49:03 -06:00
Michael Bucari-Tovo
1b2ec67726 Add series info for parent will null order. 2022-05-26 16:43:56 -06:00
Michael Bucari-Tovo
845af854bd Add exception handling to products display 2022-05-26 16:29:40 -06:00
Mbucari
15b6a66d98 Merge branch 'rmcrackan:master' into master 2022-05-26 16:13:13 -06:00
Michael Bucari-Tovo
c95ba0764b Fix bug and add groundwork for future feature 2022-05-26 16:11:52 -06:00
Robert McRackan
42c0648ba7 Bug fix #262 : 'file not found' after moved dir 2022-05-26 16:11:03 -04:00
Robert McRackan
0a6e55dcb7 * Much faster library scans
* Libraries of unlimited size now supported (prev limit was 10k)
2022-05-26 11:33:05 -04:00
rmcrackan
99b77decff Merge pull request #260 from Mbucari/master
Throttle episode scanning to 10 concurrent scans.
2022-05-26 11:29:18 -04:00
Robert McRackan
9e2ca4e586 update dependencies 2022-05-26 10:45:57 -04:00
Michael Bucari-Tovo
2e8acfdeef Limnit episode concurrency to 5 2022-05-26 08:44:39 -06:00
Michael Bucari-Tovo
630096e06d Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-26 08:43:33 -06:00
Michael Bucari-Tovo
d92d892dc7 logging 2022-05-25 20:45:18 -06:00
Michael Bucari-Tovo
a8f41841bd Throttle episode scanning to 10 concurrent scans. 2022-05-25 20:43:12 -06:00
rmcrackan
76954b5a0a Merge pull request #258 from Mbucari/master
Add support for unlimited library size.
2022-05-25 22:00:22 -04:00
Michael Bucari-Tovo
c57b184a09 Remove test params 2022-05-25 16:51:25 -06:00
Michael Bucari-Tovo
20ca4e0739 Refactor for clarity. 2022-05-25 16:49:22 -06:00
Mbucari
a972ed5e2e Merge branch 'rmcrackan:master' into master 2022-05-25 16:05:31 -06:00
Michael Bucari-Tovo
2b15bc6ebb Count Items as they come in and log total. 2022-05-25 15:11:38 -06:00
Robert McRackan
f7a482659c New feature #241 : Auto download episodes after scanning. Setting is on Import Library tab 2022-05-25 15:21:28 -04:00
Robert McRackan
99527453a7 add TODO 2022-05-25 12:56:34 -04:00
Robert McRackan
3408b4637c search engine cleanup 2022-05-25 12:49:24 -04:00
Robert McRackan
3f2899e97e * New event SearchEngineCommands.SearchEngineUpdated
* Clean up redundant event notifications
2022-05-25 10:09:27 -04:00
Michael Bucari-Tovo
562496cfaa Add more logging 2022-05-24 21:36:56 -06:00
Michael Bucari-Tovo
8283f19d6b Parallelize getChildEpisodesAsync 2022-05-24 21:17:59 -06:00
Michael Bucari-Tovo
242909b542 Don't import empty episode 2022-05-24 18:39:47 -06:00
Michael Bucari-Tovo
a7b83ad5e0 Remove 10,000 book limitation and simplify episode import 2022-05-24 18:27:20 -06:00
Michael Bucari-Tovo
ed66019d9a Cleanup 2022-05-24 18:24:53 -06:00
Michael Bucari-Tovo
bc0009be6c Use event return value instead of passing a set delegate. 2022-05-24 15:47:30 -06:00
Michael Bucari-Tovo
c88f47eed4 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-24 15:34:09 -06:00
Michael Bucari-Tovo
59de048ced Error handling network error. 2022-05-24 15:33:52 -06:00
Robert McRackan
7987dfb819 Rename 'liberate visible' menu items. Similar names are error-prone 2022-05-24 15:45:56 -04:00
Robert McRackan
1b101106e7 Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-05-24 15:32:42 -04:00
rmcrackan
7b75955aec Merge pull request #257 from Mbucari/master
Fix hang on grid update
2022-05-24 15:32:29 -04:00
Michael Bucari-Tovo
8f5467e6ca Revert stupid change. 2022-05-24 13:30:39 -06:00
Michael Bucari-Tovo
28764f92b9 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-24 13:16:56 -06:00
Michael Bucari-Tovo
777dfe4c62 Fix hang on grid update 2022-05-24 13:16:44 -06:00
Robert McRackan
0878a704d9 search engine: podcast and episode should allow plural 2022-05-24 15:07:53 -04:00
Robert McRackan
f880897542 Increm version 2022-05-24 14:04:53 -04:00
rmcrackan
b37472a954 Merge pull request #255 from Mbucari/master
Implemented Episode grouping and refactored ProductsGrid
2022-05-24 13:59:12 -04:00
Michael Bucari-Tovo
68735a45dd Change episode color 2022-05-24 11:52:33 -06:00
Michael Bucari-Tovo
e26deb9092 Address comments 2022-05-24 11:15:41 -06:00
Michael Bucari-Tovo
43d6ea82cd Change failure behavior to match previous implementation 2022-05-24 09:17:09 -06:00
Mbucari
db1aa495ac Merge branch 'rmcrackan:master' into master 2022-05-24 08:48:32 -06:00
Michael Bucari-Tovo
ee62d9ae8d Attempt to fix app hang on LogMe event 2022-05-24 07:36:17 -06:00
Robert McRackan
4001124cfa AudibleApi. Better logging around getting pdf url 2022-05-24 09:03:43 -04:00
Michael Bucari-Tovo
43a4d0d1d7 Cleanup 2022-05-23 22:24:45 -06:00
Michael Bucari-Tovo
632b432b7c Revert to old column indexing 2022-05-23 22:21:37 -06:00
Michael Bucari-Tovo
e778c7a59d Create GridView namespace 2022-05-23 21:34:43 -06:00
Michael Bucari-Tovo
d71cdecd35 Refactoring and addressing comments 2022-05-23 21:20:26 -06:00
Michael Bucari-Tovo
4a82541ffd Fix error while removing filter on a sorted binding list 2022-05-23 17:46:55 -06:00
Michael Bucari-Tovo
f29dff3386 Fix filtering bug 2022-05-23 17:22:02 -06:00
Michael Bucari-Tovo
718d21f6cb NotifyPropertyChanged series on update 2022-05-23 16:42:05 -06:00
Michael Bucari-Tovo
440550ded9 Add binding source at design time 2022-05-23 16:35:18 -06:00
Michael Bucari-Tovo
593fe57ea1 Refactor ProductsGrid 2022-05-23 15:29:26 -06:00
Michael Bucari-Tovo
e8a320dac9 Add grid categories 2022-05-22 20:00:41 -06:00
Michael Bucari-Tovo
3cb43e5d3e Improve display 2022-05-22 20:00:06 -06:00
Robert McRackan
f86bdba3c3 Test in-place upgrade 2022-05-20 16:26:58 -04:00
Robert McRackan
98c3940297 New feature ( #153 ): in-place upgrade 2022-05-20 16:20:28 -04:00
Robert McRackan
b9e789bbcf Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-05-20 14:22:09 -04:00
Robert McRackan
a108846731 Creating migrations shouldn't use file with conflicting name 2022-05-20 14:19:58 -04:00
rmcrackan
0b4ce8d6e7 Merge pull request #254 from Mbucari/master
Update installer
2022-05-18 23:02:58 -04:00
Mbucari
42df61b7dd Merge branch 'rmcrackan:master' into master 2022-05-18 17:33:10 -06:00
Michael Bucari-Tovo
6b46fa4cbc Use package installer 2022-05-18 17:32:53 -06:00
Robert McRackan
c0762eba18 Minor bug fix 2022-05-18 14:54:48 -04:00
rmcrackan
036fb848e1 Merge pull request #253 from Mbucari/master
Revert that unnecessary change
2022-05-18 14:53:47 -04:00
Mbucari
7198ae9025 Merge branch 'rmcrackan:master' into master 2022-05-18 12:49:31 -06:00
Michael Bucari-Tovo
d2822b06aa Revert "restore the old functionality to the stoplight"
This reverts commit 3648de3a8d.
2022-05-18 12:48:57 -06:00
Robert McRackan
17feca28b9 increm version 2022-05-18 14:48:46 -04:00
rmcrackan
898d38cb6a Merge pull request #252 from Mbucari/master
Fixed Issue #251
2022-05-18 14:47:40 -04:00
Michael Bucari-Tovo
95a99a2f0b Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-18 12:35:18 -06:00
Michael Bucari-Tovo
29a1e8ad34 MP3 settings always applicable for converting existing m4b 2022-05-18 12:35:12 -06:00
Mbucari
19f3a4f266 Update DownloadDecryptBook.cs 2022-05-18 12:31:32 -06:00
Michael Bucari-Tovo
12ddbc308a Fixed multipart book hanging 2022-05-18 12:29:16 -06:00
Mbucari
999bc7604e Merge branch 'rmcrackan:master' into master 2022-05-18 09:47:23 -06:00
Michael Bucari-Tovo
3648de3a8d restore the old functionality to the stoplight 2022-05-18 09:47:09 -06:00
Robert McRackan
051fa0a28f Bug fix #250 : recent refactor introduced a race condition for db creation on initial install. Moved db creation to before all other init/config is called 2022-05-18 08:13:18 -04:00
Robert McRackan
72e667e825 update dependencies 2022-05-17 17:13:20 -04:00
Robert McRackan
5ed59b41b5 fix image scaling bug when when scanning 2022-05-17 16:38:49 -04:00
Robert McRackan
c7c0d1632e Improve how highlighted index works post-filtering 2022-05-17 12:59:30 -04:00
Robert McRackan
2dc73acd20 Bug fix: incomplete refresh 2022-05-17 09:53:23 -04:00
Robert McRackan
ed71668c48 Reverted -- I might have been too hasty removing the GridEntry events 2022-05-17 08:08:53 -04:00
Robert McRackan
801e154d15 post-refactor clean up 2022-05-17 07:56:34 -04:00
rmcrackan
a89b07394f Merge pull request #249 from Mbucari/master
Add FilterableSortableBindingList to handle filtering the DataGridView
2022-05-16 22:08:31 -04:00
Mbucari
982f9b7c58 Merge branch 'rmcrackan:master' into master 2022-05-16 16:38:03 -06:00
Michael Bucari-Tovo
789b9207b5 Use that fancy patterm matching 2022-05-16 15:49:02 -06:00
Michael Bucari-Tovo
133dbb7471 Update Dinah 2022-05-16 15:11:21 -06:00
Robert McRackan
5d3ec493cd update dependencies 2022-05-16 17:06:59 -04:00
Michael Bucari-Tovo
6d7f234497 Remove unnecessary base form 2022-05-16 14:32:59 -06:00
Michael Bucari-Tovo
29a50bb640 typo 2022-05-16 14:31:03 -06:00
Michael Bucari-Tovo
843fddabde Changes discussed in email 2022-05-16 14:27:34 -06:00
Michael Bucari-Tovo
109ce0dd1f overwrite cached state 2022-05-16 14:26:43 -06:00
Robert McRackan
42508a82a0 txt file rename 2022-05-16 16:04:53 -04:00
Michael Bucari-Tovo
d860d39f5f Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-16 13:16:58 -06:00
Michael Bucari-Tovo
15396c611a Add documentation 2022-05-16 13:16:50 -06:00
Mbucari
41c4b12ae1 Merge branch 'rmcrackan:master' into master 2022-05-16 13:16:25 -06:00
Michael Bucari-Tovo
e51c30462f Revert "Use new ProcessQueue"
This reverts commit 9b5df99a61.
2022-05-16 13:16:11 -06:00
Michael Bucari-Tovo
9b5df99a61 Use new ProcessQueue 2022-05-16 12:56:15 -06:00
Michael Bucari-Tovo
3535156ea5 Edit 2022-05-16 12:47:50 -06:00
Robert McRackan
577145096d * GridEntry.DownloadBook is no longer called. it was the only one calling UpdateLiberatedStatus(true) or using DownloadInProgress flag
* cleaned up unused code, old forms, ProcessorAutomationController...
* what's left of LogMe and ProcessorAutomationController should be moved eventually
2022-05-16 14:44:17 -04:00
Michael Bucari-Tovo
89059510fd More logical naming 2022-05-16 12:37:56 -06:00
Michael Bucari-Tovo
aabc14c639 Make AllItems a method 2022-05-16 12:12:34 -06:00
Michael Bucari-Tovo
c28872544c Don't call concat for every book. 2022-05-16 12:10:54 -06:00
Michael Bucari-Tovo
7b8a4e4d72 Simplify filtering 2022-05-16 12:06:56 -06:00
Michael Bucari-Tovo
5dcdf670be Simplify RemoveFilter 2022-05-16 11:58:36 -06:00
Michael Bucari-Tovo
9721890a3c Update documentation 2022-05-16 11:50:11 -06:00
Michael Bucari-Tovo
1b9c4cfc23 Remove unused usings 2022-05-16 11:47:34 -06:00
Michael Bucari-Tovo
98a552e9af Optimization 2022-05-16 11:46:42 -06:00
Michael Bucari-Tovo
e1e265a101 Don't filter after every insert 2022-05-16 11:38:24 -06:00
Robert McRackan
b60a854de0 Formattable UI labels 2022-05-16 13:34:49 -04:00
Michael Bucari-Tovo
d1bddeccc8 Implement filtering in the sortable binding list. 2022-05-16 11:16:33 -06:00
Robert McRackan
0a106e64d8 liberate visible to use new process queue 2022-05-16 08:20:40 -04:00
Michael Bucari-Tovo
91d6181aec Better naming 2022-05-15 20:15:54 -06:00
Michael Bucari-Tovo
255c0a3359 Move filtering into SyncBindingSource 2022-05-15 19:58:59 -06:00
Robert McRackan
3a5ef999f0 Bug fix: fatal exception if no large picture 2022-05-15 15:38:36 -04:00
rmcrackan
983aa845d6 Merge pull request #247 from Mbucari/master
Fixed scaling issue
2022-05-15 15:21:05 -04:00
Robert McRackan
d1779726e6 New __ARCHITECTURE NOTES.txt incl. MVVM comments 2022-05-15 15:13:25 -04:00
Michael Bucari-Tovo
8e23062d0e Fix scaling for all display scalings 2022-05-15 13:12:46 -06:00
Michael Bucari-Tovo
7efbfffd99 Fixed scaling issue. 2022-05-15 12:47:39 -06:00
rmcrackan
ff4b2d2ecc Merge pull request #244 from Mbucari/master
New Processing Queue
2022-05-15 14:25:52 -04:00
Michael Bucari-Tovo
e079be0ad7 Make scrll look more natural when removing items from control 2022-05-15 11:27:49 -06:00
Michael Bucari-Tovo
a8a54aa443 Revert "Make scrll look more natural when removing items from control"
This reverts commit 88cbcf6baf.
2022-05-15 11:26:07 -06:00
Michael Bucari-Tovo
88cbcf6baf Make scrll look more natural when removing items from control 2022-05-15 11:25:33 -06:00
Michael Bucari-Tovo
8d6d26c9d2 Improve logging 2022-05-15 11:16:41 -06:00
Michael Bucari-Tovo
a490df0f7e Fix possible index range error 2022-05-15 11:10:37 -06:00
Michael Bucari-Tovo
a46041c958 More useful logging 2022-05-15 09:58:36 -06:00
Michael Bucari-Tovo
0a6a78bc58 Revert "More useful logging"
This reverts commit c9e850515e.
2022-05-15 09:56:56 -06:00
Michael Bucari-Tovo
c9e850515e More useful logging 2022-05-15 09:56:46 -06:00
Michael Bucari-Tovo
0ff8da2cf0 Add await and make cancel async 2022-05-15 09:30:44 -06:00
Michael Bucari-Tovo
c0ef3ccbea Tiny bugfix 2022-05-15 09:00:52 -06:00
Michael Bucari-Tovo
1ab628dee8 Better invocation. Post instead of Send 2022-05-14 23:45:13 -06:00
Michael Bucari-Tovo
b24df24b10 Detect Conversion cancelled 2022-05-14 23:44:15 -06:00
Michael Bucari-Tovo
341678d979 Remove my testing code. oops. 2022-05-14 20:47:13 -06:00
Michael Bucari-Tovo
49d10273a6 Add button to hide queue 2022-05-14 20:44:53 -06:00
Michael Bucari-Tovo
5b05c018d5 Remove ValidationFail books from queue display. Nothing to see there. 2022-05-14 20:00:23 -06:00
Michael Bucari-Tovo
d18d8c0ba4 Filter 2022-05-14 16:23:34 -06:00
Michael Bucari-Tovo
84a8fb0074 Minor refactor 2022-05-14 16:13:19 -06:00
Michael Bucari-Tovo
a40fb7f4bd Colorsto variables 2022-05-14 15:57:36 -06:00
Michael Bucari-Tovo
84eb3a3508 Remove debug button 2022-05-14 15:33:06 -06:00
Michael Bucari-Tovo
73a5d76503 Make thread safe and integrate with Libation UI 2022-05-14 14:39:46 -06:00
Michael Bucari-Tovo
50c35ed519 Change log to gridview and new INotifyPropertyChanged event 2022-05-14 13:52:54 -06:00
Michael Bucari-Tovo
a7b7e3efea Converted to INotifyPropertyChanged for more targeted view update 2022-05-14 13:52:10 -06:00
Michael Bucari-Tovo
88e892196f Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-14 11:20:30 -06:00
Michael Bucari-Tovo
7f08da96bb Documentation and organization 2022-05-14 11:20:19 -06:00
Mbucari
193f24768e Merge branch 'rmcrackan:master' into master 2022-05-14 11:15:23 -06:00
Michael Bucari-Tovo
a8bca3de98 Fix progressbar wiggling 2022-05-14 11:11:51 -06:00
Michael Bucari-Tovo
9692a802d0 Update documentation and add parameters 2022-05-14 11:11:20 -06:00
Robert McRackan
28a8b2e685 Revert: only call notifyPropertyChanged if actually set to new value 2022-05-14 12:34:01 -04:00
Michael Bucari-Tovo
3c9121b4af Improve scroll visualization 2022-05-14 10:03:57 -06:00
Michael Bucari-Tovo
dec1035258 Minor UI tweak 2022-05-14 04:11:44 -06:00
Michael Bucari-Tovo
9d81c86c1b Increase buffer size 2022-05-14 04:10:54 -06:00
Michael Bucari-Tovo
eeb4f4681a Saved 2022-05-14 03:16:48 -06:00
Michael Bucari-Tovo
676af0210b Finalized ProcessBookControl 2022-05-14 02:54:32 -06:00
Michael Bucari-Tovo
77c6a2890b Finalized VirtualFlowControl 2022-05-14 02:54:09 -06:00
Michael Bucari-Tovo
c39e748749 Finialized TrackedQueue 2022-05-14 01:33:05 -06:00
Mbucari
36e5a6ac8d Merge branch 'rmcrackan:master' into master 2022-05-13 21:00:56 -06:00
Robert McRackan
9bdcaa5eaa only call notifyPropertyChanged if actually set to new value 2022-05-13 16:30:46 -04:00
Mbucari
5511004db8 Merge branch 'rmcrackan:master' into master 2022-05-13 14:09:29 -06:00
Robert McRackan
0e46cdb514 refactor Form1. too much in 1 file 2022-05-13 13:39:49 -04:00
Mbucari
b028899949 Merge branch 'rmcrackan:master' into master 2022-05-13 11:12:37 -06:00
Robert McRackan
55285427f1 Add series in default search 2022-05-13 11:42:08 -04:00
Michael Bucari-Tovo
763a6cb31a Added VirtualFlowControl and BookQueue 2022-05-13 00:21:41 -06:00
Robert McRackan
24cb1aa84f remove legacy references 2022-05-12 13:35:08 -04:00
rmcrackan
886aa4938d Merge pull request #243 from Mbucari/master
Minor mods for future UI changes
2022-05-12 13:30:30 -04:00
Michael Bucari-Tovo
8871651549 Change namespace/folder name 2022-05-12 11:17:14 -06:00
Michael Bucari-Tovo
2ae8ef87d9 Remove unised 2022-05-12 11:06:53 -06:00
Michael Bucari-Tovo
de4fbe05f7 Remove old holdover 2022-05-12 11:01:41 -06:00
Michael Bucari-Tovo
b8abed37c2 Merged popout with main brainch. 2022-05-12 10:52:55 -06:00
Mbucari
255e26435c Merge branch 'rmcrackan:master' into master 2022-05-12 10:32:05 -06:00
Michael Bucari-Tovo
9e0550619b Merging 2022-05-12 10:31:50 -06:00
Michael Bucari-Tovo
5c171fd0f0 Merging 2022-05-12 10:29:44 -06:00
Michael Bucari-Tovo
3dd3b710b7 merge fix 2022-05-12 10:17:29 -06:00
Robert McRackan
bce3bdba7e Feature requests #229 , #148 : Bulk actions on filtered books 2022-05-12 11:57:56 -04:00
Robert McRackan
360f077da3 * fixed where the filter was being called multiple times on launch
* simplified productsGrid init means a lot of defensive code is no longer needed
2022-05-12 10:28:30 -04:00
Robert McRackan
75c5f662dc * Batch actions for visible books: 'remove from library' complete
* refactor entity properties into extension methods
* refactor shared simple message boxes => MessageBoxLib
2022-05-12 09:53:21 -04:00
Michael Bucari-Tovo
3c0485cfa9 Prepare Form1 for new docking queue 2022-05-12 00:10:13 -06:00
Michael Bucari-Tovo
d5ba405de0 MatchCurrent 2022-05-11 23:44:33 -06:00
Michael Bucari-Tovo
71b8bca86d Revert "Revert "Changes for dockable process""
This reverts commit 6d6434b4d4.
2022-05-11 21:12:18 -06:00
Mbucari
c53b9eabd6 Merge branch 'rmcrackan:master' into master 2022-05-11 21:11:44 -06:00
Michael Bucari-Tovo
6d6434b4d4 Revert "Changes for dockable process"
This reverts commit a447e88b86.
2022-05-11 21:11:25 -06:00
Michael Bucari-Tovo
a447e88b86 Changes for dockable process 2022-05-11 21:10:02 -06:00
Michael Bucari-Tovo
e2d2e00913 Fix conflict? 2022-05-11 21:07:32 -06:00
Michael Bucari-Tovo
cbfea37b3a Fix conflict 2022-05-11 21:06:01 -06:00
Michael Bucari-Tovo
d6de647974 Fix conflict 2022-05-11 21:04:11 -06:00
Robert McRackan
b784bd6b8d Batch actions for visible books: LIberate complete 2022-05-11 22:16:15 -04:00
Robert McRackan
00df6da366 'visible books' only enabled when applicable 2022-05-11 21:47:48 -04:00
Michael Bucari-Tovo
dad36c73e5 Expose grid entries 2022-05-11 18:45:57 -06:00
Michael Bucari-Tovo
936a1d60a0 Minor mods for future UI changes 2022-05-11 18:36:48 -06:00
rmcrackan
e0248c2d8e Merge pull request #242 from Mbucari/master
Add option to download cover art & full-size cover art viewer
2022-05-11 10:53:11 -04:00
Robert McRackan
b12731e3d5 resolve merge conflict 2022-05-11 10:52:46 -04:00
Robert McRackan
9636aca47c update grid-visible in main form 2022-05-11 10:50:19 -04:00
Robert McRackan
4138183352 improve grid 'visible' 2022-05-11 10:49:41 -04:00
Robert McRackan
c3871d3bca * Bug fix: grid doesn't update correctly if all books are removed
* Beginning (incomplete) new menu for batch actions on visible books
2022-05-11 10:13:07 -04:00
Michael Bucari-Tovo
dd8b0783a9 Address comments 2022-05-10 20:43:07 -06:00
Michael Bucari-Tovo
9a50aa4c7c Start task earlier. 2022-05-10 16:11:44 -06:00
Michael Bucari-Tovo
c40185030f Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-10 16:11:09 -06:00
Michael Bucari-Tovo
7cba28019c Remove lambda body 2022-05-10 16:10:17 -06:00
Mbucari
926f8a957e Merge branch 'rmcrackan:master' into master 2022-05-10 16:03:52 -06:00
Michael Bucari-Tovo
59aeaf24e4 Full-size cover picture viewer 2022-05-10 16:03:02 -06:00
Michael Bucari-Tovo
64eaa157e5 Add option for downloading cover 2022-05-10 15:36:31 -06:00
Michael Bucari-Tovo
9a5d9f3867 Move cover art downloader to DownloadDecryptBook 2022-05-10 15:25:47 -06:00
Robert McRackan
e368e4669b bug fix: db persistence shouldn't be a side effect. the client should say when to persist in some fairly explicit way 2022-05-10 16:48:52 -04:00
Robert McRackan
c6ce814e1c Set the stage for batch processing 2022-05-10 16:18:09 -04:00
Robert McRackan
dd5e162c10 db persistence shouldn't be a side effect. the client should say when to persist in some fairly explicit way 2022-05-10 16:17:12 -04:00
Robert McRackan
7af890d897 GetLibrary to include image sizes 500, 1215 2022-05-10 15:32:00 -04:00
Robert McRackan
0faeeea25f update dependencies 2022-05-10 14:50:34 -04:00
rmcrackan
de9b3fd6ec Merge pull request #240 from Mbucari/master
Lots of new UI stuff!
2022-05-10 14:37:39 -04:00
Michael Bucari-Tovo
22e5c8746c Updated Api 2022-05-10 12:36:46 -06:00
Michael Bucari-Tovo
0091245734 Revert PictureID_1215 migration and add PictureIDLarge migration 2022-05-10 12:03:47 -06:00
Michael Bucari-Tovo
448c231cfa Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-10 12:02:28 -06:00
Michael Bucari-Tovo
b0d1f692a3 Use new PictureIDLarge and PictureSize.Native 2022-05-10 12:01:23 -06:00
Mbucari
a5ff890ea1 Delete LibationContext.db 2022-05-10 11:42:24 -06:00
Michael Bucari-Tovo
df4739cbf4 Set description cell tooltip text 2022-05-10 08:40:42 -06:00
Michael Bucari-Tovo
9559109aa8 Fix liberated status cache 2022-05-10 00:27:23 -06:00
Michael Bucari-Tovo
d848c1a499 Cache book and pdf statuses for faster sorting. 2022-05-10 00:04:55 -06:00
Michael Bucari-Tovo
48ffc40abb Move cover download to processable 2022-05-09 23:53:10 -06:00
Michael Bucari-Tovo
82b5daa809 Download large cover art after successfilly downloading an audiobook. 2022-05-09 23:22:21 -06:00
Michael Bucari-Tovo
b320276926 Add PictureID_1215 to Book and migrate DB 2022-05-09 23:21:52 -06:00
Michael Bucari-Tovo
6ccb8d612f Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-09 22:16:24 -06:00
Michael Bucari-Tovo
23460e0137 Don't show description form in task bar 2022-05-09 22:09:47 -06:00
Mbucari
7723de7284 Update ProductsGrid.cs 2022-05-09 21:49:56 -06:00
Michael Bucari-Tovo
138f94594f Fixed cross-thread access error. 2022-05-09 21:31:21 -06:00
Michael Bucari-Tovo
81c152ddcb Add libation icon to forms 2022-05-09 21:12:41 -06:00
Michael Bucari-Tovo
04665fea36 Change default border width 2022-05-09 20:37:41 -06:00
Michael Bucari-Tovo
803eef3825 Settings are not mutually exclusive 2022-05-09 20:37:05 -06:00
Michael Bucari-Tovo
e2a05761a6 Unnecessary. Cleanup of unfinished decrypts is performed by Caller 2022-05-09 20:36:48 -06:00
Michael Bucari-Tovo
b1968caa0f Improve display and fix display indices 2022-05-09 20:29:08 -06:00
Michael Bucari-Tovo
6474ef98f5 Fix description form location 2022-05-09 18:10:09 -06:00
Michael Bucari-Tovo
8763d63a93 Better windows positioning 2022-05-09 17:15:16 -06:00
Michael Bucari-Tovo
201ecebda9 Show full book description when Description cell is clicked. 2022-05-09 16:44:44 -06:00
Michael Bucari-Tovo
1c9ea0a710 No text trimming since columns are enlargeable 2022-05-09 15:44:37 -06:00
Michael Bucari-Tovo
30feb42ed8 Add setting to persist ProductsGrid column widths 2022-05-09 15:30:18 -06:00
Mbucari
cfe2eac351 Merge branch 'rmcrackan:master' into master 2022-05-09 14:40:18 -06:00
Michael Bucari-Tovo
725979afb0 Improve Context Menu performance. 2022-05-09 14:38:14 -06:00
Michael Bucari-Tovo
19262bceac Fix display issue if all columns are hidden on startup 2022-05-09 14:16:14 -06:00
Mbucari
5ad1e45c65 Merge branch 'rmcrackan:master' into master 2022-05-09 13:21:52 -06:00
Michael Bucari-Tovo
9fe95bbddc Add option to reorder ProductsGrid columns 2022-05-09 13:21:10 -06:00
Michael Bucari-Tovo
aecc54401d Add option for user to hide columns in ProductsGrid 2022-05-09 12:58:09 -06:00
176 changed files with 12367 additions and 4031 deletions

View File

@@ -40,7 +40,7 @@ Libation's advanced searching is built on the powerful Lucene search engine. Sim
* Full official guide: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html
* Tons of search fields, specific to audiobooks
* Synonyms so you don't have to memorize magic words. Eg: author and author**s** will both work
* Click [?] button for a full list of search fields and synonyms ![Filter options](images/FilterOptions.png)
* Click [?] button for a full list of search fields and synonyms ![Filter options](images/FilterOptionsButton.png)
* Search by tag like \[this\]
* When tags have an underscore you can use part of the tag. This is useful for quick categories. The below examples make this more clear.

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean" Version="0.4.6" />
<PackageReference Include="AAXClean" Version="0.4.7" />
<PackageReference Include="AAXClean.Codecs" Version="0.2.7" />
</ItemGroup>

View File

@@ -20,7 +20,7 @@ namespace AaxDecrypter
public event EventHandler<TimeSpan> DecryptTimeRemaining;
public event EventHandler<string> FileCreated;
protected bool IsCanceled { get; set; }
public bool IsCanceled { get; set; }
protected string OutputFileName { get; private set; }
protected DownloadOptions DownloadOptions { get; }
@@ -140,11 +140,6 @@ namespace AaxDecrypter
else
FileUtility.SaferDelete(TempFilePath);
}
else
{
FileUtility.SaferDelete(OutputFileName);
}
return success;
}

View File

@@ -9,435 +9,442 @@ using System.Threading;
namespace AaxDecrypter
{
/// <summary>
/// A <see cref="CookieContainer"/> for a single Uri.
/// </summary>
public class SingleUriCookieContainer : CookieContainer
{
private Uri baseAddress;
public Uri Uri
{
get => baseAddress;
set
{
baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
}
}
public CookieCollection GetCookies()
{
return GetCookies(Uri);
}
}
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
public class NetworkFileStream : Stream, IUpdatable
{
public event EventHandler Updated;
#region Public Properties
/// <summary>
/// Location to save the downloaded data.
/// </summary>
[JsonProperty(Required = Required.Always)]
public string SaveFilePath { get; }
/// <summary>
/// Http(s) address of the file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; }
/// <summary>
/// All cookies set by caller or by the remote server.
/// </summary>
[JsonProperty(Required = Required.Always)]
public SingleUriCookieContainer CookieContainer { get; }
/// <summary>
/// Http headers to be sent to the server with the request.
/// </summary>
[JsonProperty(Required = Required.Always)]
public WebHeaderCollection RequestHeaders { get; private set; }
/// <summary>
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long WritePosition { get; private set; }
/// <summary>
/// The total length of the <see cref="Uri"/> file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; }
#endregion
#region Private Properties
private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; }
private FileStream _readFile { get; }
private Stream _networkStream { get; set; }
private bool hasBegunDownloading { get; set; }
public bool IsCancelled { get; private set; }
private EventWaitHandle downloadEnded { get; set; }
private EventWaitHandle downloadedPiece { get; set; }
#endregion
#region Constants
//Download buffer size
private const int DOWNLOAD_BUFF_SZ = 4 * 1024;
//NetworkFileStream will flush all data in _writeFile to disk after every
//DATA_FLUSH_SZ bytes are written to the file stream.
private const int DATA_FLUSH_SZ = 1024 * 1024;
#endregion
#region Constructor
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
/// <param name="uri">Http(s) address of the file to download.</param>
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param>
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
SaveFilePath = saveFilePath;
Uri = uri;
WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new WebHeaderCollection();
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{
Position = WritePosition
};
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
SetUriForSameFile(uri);
}
#endregion
#region Downloader
/// <summary>
/// Update the <see cref="JsonFilePersister"/>.
/// </summary>
private void Update()
{
RequestHeaders = HttpRequest.Headers;
Updated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
/// </summary>
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
public void SetUriForSameFile(Uri uriToSameFile)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (hasBegunDownloading)
throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile;
HttpRequest = WebRequest.CreateHttp(Uri);
HttpRequest.CookieContainer = CookieContainer;
HttpRequest.Headers = RequestHeaders;
//If NetworkFileStream is resuming, Header will already contain a range.
HttpRequest.Headers.Remove("Range");
HttpRequest.AddRange(WritePosition);
}
/// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary>
private void BeginDownloading()
{
downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
if (ContentLength != 0 && WritePosition == ContentLength)
{
hasBegunDownloading = true;
downloadEnded.Set();
return;
}
if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
var response = HttpRequest.GetResponse() as HttpWebResponse;
if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
//Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0-
if (WritePosition == 0)
ContentLength = response.ContentLength;
_networkStream = response.GetResponseStream();
downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background.
new Thread(() => DownloadFile())
{ IsBackground = true }
.Start();
hasBegunDownloading = true;
return;
}
/// <summary>
/// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>.
/// </summary>
private void DownloadFile()
{
var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
var buff = new byte[DOWNLOAD_BUFF_SZ];
do
{
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead;
if (downloadPosition > nextFlush)
{
_writeFile.Flush();
WritePosition = downloadPosition;
Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set();
}
} while (downloadPosition < ContentLength && !IsCancelled);
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition;
Update();
downloadedPiece.Set();
downloadEnded.Set();
if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
}
#endregion
#region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings()
{
var settings = new JsonSerializerSettings();
settings.Converters.Add(new CookieContainerConverter());
settings.Converters.Add(new WebHeaderCollectionConverter());
return settings;
}
internal class CookieContainerConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(SingleUriCookieContainer);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new SingleUriCookieContainer()
{
Uri = new Uri(jObj["Uri"].Value<string>()),
Capacity = jObj["Capacity"].Value<int>(),
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
};
var cookieList = jObj["Cookies"].ToList();
foreach (var cookie in cookieList)
{
result.Add(
new Cookie
{
Comment = cookie["Comment"].Value<string>(),
HttpOnly = cookie["HttpOnly"].Value<bool>(),
Discard = cookie["Discard"].Value<bool>(),
Domain = cookie["Domain"].Value<string>(),
Expired = cookie["Expired"].Value<bool>(),
Expires = cookie["Expires"].Value<DateTime>(),
Name = cookie["Name"].Value<string>(),
Path = cookie["Path"].Value<string>(),
Port = cookie["Port"].Value<string>(),
Secure = cookie["Secure"].Value<bool>(),
Value = cookie["Value"].Value<string>(),
Version = cookie["Version"].Value<int>(),
});
}
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var cookies = value as SingleUriCookieContainer;
var obj = (JObject)JToken.FromObject(value);
var container = cookies.GetCookies();
var propertyNames = container.Select(c => JToken.FromObject(c));
obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
obj.WriteTo(writer);
}
}
internal class WebHeaderCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(WebHeaderCollection);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new WebHeaderCollection();
foreach (var kvp in jObj)
result.Add(kvp.Key, kvp.Value.Value<string>());
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var jObj = new JObject();
var type = value.GetType();
var headers = value as WebHeaderCollection;
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
jObj.Add(jHeaders);
jObj.WriteTo(writer);
}
}
#endregion
#region Download Stream Reader
[JsonIgnore]
public override bool CanRead => true;
[JsonIgnore]
public override bool CanSeek => true;
[JsonIgnore]
public override bool CanWrite => false;
[JsonIgnore]
public override long Length
{
get
{
if (!hasBegunDownloading)
BeginDownloading();
return ContentLength;
}
}
[JsonIgnore]
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
[JsonIgnore]
public override bool CanTimeout => false;
[JsonIgnore]
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
[JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
public override void Flush() => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
public override int Read(byte[] buffer, int offset, int count)
{
if (!hasBegunDownloading)
BeginDownloading();
var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead);
return _readFile.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
var newPosition = origin switch
/// <summary>
/// A <see cref="CookieContainer"/> for a single Uri.
/// </summary>
public class SingleUriCookieContainer : CookieContainer
{
private Uri baseAddress;
public Uri Uri
{
get => baseAddress;
set
{
baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
}
}
public CookieCollection GetCookies()
{
return GetCookies(Uri);
}
}
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
public class NetworkFileStream : Stream, IUpdatable
{
public event EventHandler Updated;
#region Public Properties
/// <summary>
/// Location to save the downloaded data.
/// </summary>
[JsonProperty(Required = Required.Always)]
public string SaveFilePath { get; }
/// <summary>
/// Http(s) address of the file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; }
/// <summary>
/// All cookies set by caller or by the remote server.
/// </summary>
[JsonProperty(Required = Required.Always)]
public SingleUriCookieContainer CookieContainer { get; }
/// <summary>
/// Http headers to be sent to the server with the request.
/// </summary>
[JsonProperty(Required = Required.Always)]
public WebHeaderCollection RequestHeaders { get; private set; }
/// <summary>
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long WritePosition { get; private set; }
/// <summary>
/// The total length of the <see cref="Uri"/> file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; }
#endregion
#region Private Properties
private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; }
private FileStream _readFile { get; }
private Stream _networkStream { get; set; }
private bool hasBegunDownloading { get; set; }
public bool IsCancelled { get; private set; }
private EventWaitHandle downloadEnded { get; set; }
private EventWaitHandle downloadedPiece { get; set; }
#endregion
#region Constants
//Download buffer size
private const int DOWNLOAD_BUFF_SZ = 32 * 1024;
//NetworkFileStream will flush all data in _writeFile to disk after every
//DATA_FLUSH_SZ bytes are written to the file stream.
private const int DATA_FLUSH_SZ = 1024 * 1024;
#endregion
#region Constructor
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
/// <param name="uri">Http(s) address of the file to download.</param>
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param>
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
SaveFilePath = saveFilePath;
Uri = uri;
WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new WebHeaderCollection();
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{
Position = WritePosition
};
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
SetUriForSameFile(uri);
}
#endregion
#region Downloader
/// <summary>
/// Update the <see cref="JsonFilePersister"/>.
/// </summary>
private void Update()
{
RequestHeaders = HttpRequest.Headers;
Updated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
/// </summary>
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
public void SetUriForSameFile(Uri uriToSameFile)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (hasBegunDownloading)
throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile;
HttpRequest = WebRequest.CreateHttp(Uri);
HttpRequest.CookieContainer = CookieContainer;
HttpRequest.Headers = RequestHeaders;
//If NetworkFileStream is resuming, Header will already contain a range.
HttpRequest.Headers.Remove("Range");
HttpRequest.AddRange(WritePosition);
}
/// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary>
private void BeginDownloading()
{
downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
if (ContentLength != 0 && WritePosition == ContentLength)
{
hasBegunDownloading = true;
downloadEnded.Set();
return;
}
if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
var response = HttpRequest.GetResponse() as HttpWebResponse;
if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
//Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0-
if (WritePosition == 0)
ContentLength = response.ContentLength;
_networkStream = response.GetResponseStream();
downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background.
new Thread(() => DownloadFile())
{ IsBackground = true }
.Start();
hasBegunDownloading = true;
return;
}
/// <summary>
/// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>.
/// </summary>
private void DownloadFile()
{
var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
var buff = new byte[DOWNLOAD_BUFF_SZ];
try
{
do
{
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead;
if (downloadPosition > nextFlush)
{
_writeFile.Flush();
WritePosition = downloadPosition;
Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set();
}
} while (downloadPosition < ContentLength && !IsCancelled);
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition;
Update();
downloadedPiece.Set();
downloadEnded.Set();
if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri);
IsCancelled = true;
}
}
#endregion
#region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings()
{
var settings = new JsonSerializerSettings();
settings.Converters.Add(new CookieContainerConverter());
settings.Converters.Add(new WebHeaderCollectionConverter());
return settings;
}
internal class CookieContainerConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(SingleUriCookieContainer);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new SingleUriCookieContainer()
{
Uri = new Uri(jObj["Uri"].Value<string>()),
Capacity = jObj["Capacity"].Value<int>(),
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
};
var cookieList = jObj["Cookies"].ToList();
foreach (var cookie in cookieList)
{
result.Add(
new Cookie
{
Comment = cookie["Comment"].Value<string>(),
HttpOnly = cookie["HttpOnly"].Value<bool>(),
Discard = cookie["Discard"].Value<bool>(),
Domain = cookie["Domain"].Value<string>(),
Expired = cookie["Expired"].Value<bool>(),
Expires = cookie["Expires"].Value<DateTime>(),
Name = cookie["Name"].Value<string>(),
Path = cookie["Path"].Value<string>(),
Port = cookie["Port"].Value<string>(),
Secure = cookie["Secure"].Value<bool>(),
Value = cookie["Value"].Value<string>(),
Version = cookie["Version"].Value<int>(),
});
}
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var cookies = value as SingleUriCookieContainer;
var obj = (JObject)JToken.FromObject(value);
var container = cookies.GetCookies();
var propertyNames = container.Select(c => JToken.FromObject(c));
obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
obj.WriteTo(writer);
}
}
internal class WebHeaderCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(WebHeaderCollection);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new WebHeaderCollection();
foreach (var kvp in jObj)
result.Add(kvp.Key, kvp.Value.Value<string>());
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var jObj = new JObject();
var type = value.GetType();
var headers = value as WebHeaderCollection;
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
jObj.Add(jHeaders);
jObj.WriteTo(writer);
}
}
#endregion
#region Download Stream Reader
[JsonIgnore]
public override bool CanRead => true;
[JsonIgnore]
public override bool CanSeek => true;
[JsonIgnore]
public override bool CanWrite => false;
[JsonIgnore]
public override long Length
{
get
{
if (!hasBegunDownloading)
BeginDownloading();
return ContentLength;
}
}
[JsonIgnore]
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
[JsonIgnore]
public override bool CanTimeout => false;
[JsonIgnore]
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
[JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
public override void Flush() => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
public override int Read(byte[] buffer, int offset, int count)
{
if (!hasBegunDownloading)
BeginDownloading();
var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead);
return _readFile.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
var newPosition = origin switch
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => ContentLength + offset,
_ => offset,
};
WaitToPosition(newPosition);
return _readFile.Position = newPosition;
}
WaitToPosition(newPosition);
return _readFile.Position = newPosition;
}
/// <summary>
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
/// <summary>
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
{
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
}
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
}
public override void Close()
{
IsCancelled = true;
public override void Close()
{
IsCancelled = true;
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
_readFile.Close();
_writeFile.Close();
_networkStream?.Close();
Update();
}
_readFile.Close();
_writeFile.Close();
_networkStream?.Close();
Update();
}
#endregion
~NetworkFileStream()
{
downloadEnded?.Close();
downloadedPiece?.Close();
}
}
#endregion
~NetworkFileStream()
{
downloadEnded?.Close();
downloadedPiece?.Close();
}
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>7.3.0.1</Version>
<Version>7.11.0.1</Version>
</PropertyGroup>
<ItemGroup>
@@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using ApplicationServices;
using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.IO;
@@ -36,7 +37,8 @@ namespace AppScaffolding
public static Configuration RunPreConfigMigrations()
{
// must occur before access to Configuration instance
Migrations.migrate_to_v5_2_0__pre_config();
// // outdated. kept here as an example of what belongs in this area
// // Migrations.migrate_to_v5_2_0__pre_config();
//***********************************************//
// //
@@ -57,6 +59,7 @@ namespace AppScaffolding
//
Migrations.migrate_to_v6_6_9(config);
Migrations.migrate_from_7_10_1(config);
}
public static void PopulateMissingConfigValues(Configuration config)
@@ -125,14 +128,31 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.AutoScan)))
config.AutoScan = true;
if (!config.Exists(nameof(config.GridColumnsVisibilities)))
config.GridColumnsVisibilities = new Dictionary<string, bool>();
if (!config.Exists(nameof(config.GridColumnsDisplayIndices)))
config.GridColumnsDisplayIndices = new Dictionary<string, int>();
if (!config.Exists(nameof(config.GridColumnsWidths)))
config.GridColumnsWidths = new Dictionary<string, int>();
if (!config.Exists(nameof(config.DownloadCoverArt)))
config.DownloadCoverArt = true;
if (!config.Exists(nameof(config.AutoDownloadEpisodes)))
config.AutoDownloadEpisodes = false;
}
/// <summary>Initialize logging. Run after migration</summary>
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
public static void RunPostMigrationScaffolding(Configuration config)
{
ensureSerilogConfig(config);
configureLogging(config);
logStartupState(config);
wireUpSystemEvents(config);
}
private static void ensureSerilogConfig(Configuration config)
@@ -238,18 +258,21 @@ namespace AppScaffolding
private static void logStartupState(Configuration config)
{
#if DEBUG
var mode = "Debug";
#else
var mode = "Release";
#endif
if (System.Diagnostics.Debugger.IsAttached)
mode += " (Debugger attached)";
// begin logging session with a form feed
Log.Logger.Information("\r\n\f");
Log.Logger.Information("Begin. {@DebugInfo}", new
{
AppName = EntryAssembly.GetName().Name,
Version = BuildVersion.ToString(),
#if DEBUG
Mode = "Debug",
#else
Mode = "Release",
#endif
Mode = mode,
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
@@ -270,22 +293,26 @@ namespace AppScaffolding
});
}
public static (bool hasUpgrade, string zipUrl, string htmlUrl, string zipName) GetLatestRelease()
private static void wireUpSystemEvents(Configuration configuration)
{
(bool, string, string, string) isFalse = (false, null, null, null);
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
}
public static UpgradeProperties GetLatestRelease()
{
// timed out
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
if (latest is null)
return isFalse;
return null;
var latestVersionString = latest.TagName.Trim('v');
if (!Version.TryParse(latestVersionString, out var latestRelease))
return isFalse;
return null;
// we're up to date
if (latestRelease <= BuildVersion)
return isFalse;
return null;
// we have an update
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
@@ -298,7 +325,7 @@ namespace AppScaffolding
zipUrl
});
return (true, zipUrl, latest.HtmlUrl, zip.Name);
return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease);
}
private static Octokit.Release getLatestRelease(TimeSpan timeout)
{
@@ -329,41 +356,6 @@ namespace AppScaffolding
internal static class Migrations
{
#region migrate to v5.2.0
// get rid of meta-directories, combine DownloadsInProgressEnum and DecryptInProgressEnum => InProgress
public static void migrate_to_v5_2_0__pre_config()
{
{
var settingsKey = "DownloadsInProgressEnum";
if (UNSAFE_MigrationHelper.Settings_TryGet(settingsKey, out var value))
{
UNSAFE_MigrationHelper.Settings_Delete(settingsKey);
UNSAFE_MigrationHelper.Settings_Insert("InProgress", translatePath(value));
}
}
{
UNSAFE_MigrationHelper.Settings_Delete("DecryptInProgressEnum");
}
{ // appsettings.json
var appSettingsKey = UNSAFE_MigrationHelper.LIBATION_FILES_KEY;
if (UNSAFE_MigrationHelper.APPSETTINGS_TryGet(appSettingsKey, out var value))
UNSAFE_MigrationHelper.APPSETTINGS_Update(appSettingsKey, translatePath(value));
}
}
private static string translatePath(string path)
=> path switch
{
"AppDir" => @".\LibationFiles",
"MyDocs" => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "LibationFiles")),
"UserProfile" => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation")),
"WinTemp" => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")),
_ => path
};
#endregion
public static void migrate_to_v6_6_9(Configuration config)
{
var writeToPath = $"Serilog.WriteTo";
@@ -410,5 +402,28 @@ namespace AppScaffolding
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
}
}
public static void migrate_from_7_10_1(Configuration config)
{
//This migration removes books and series with SERIES_ prefix that were created
//as a hack workaround in 7.10.1. Said workaround was removed in 7.10.2
var migrated = config.GetNonString<bool>(nameof(migrate_from_7_10_1));
if (migrated) return;
using var context = DbContexts.GetContext();
var booksToRemove = context.Books.Where(b => b.AudibleProductId.StartsWith("SERIES_")).ToArray();
var seriesToRemove = context.Series.Where(s => s.AudibleSeriesId.StartsWith("SERIES_")).ToArray();
var lbToRemove = context.LibraryBooks.Where(lb => booksToRemove.Any(b => b == lb.Book)).ToArray();
context.LibraryBooks.RemoveRange(lbToRemove);
context.Books.RemoveRange(booksToRemove);
context.Series.RemoveRange(seriesToRemove);
LibraryCommands.SaveContext(context);
config.SetObject(nameof(migrate_from_7_10_1), true);
}
}
}

View File

@@ -17,8 +17,13 @@ namespace AppScaffolding
///
///
/// </summary>
internal static class UNSAFE_MigrationHelper
public static class UNSAFE_MigrationHelper
{
public static string SettingsDirectory
=> !APPSETTINGS_TryGet(LIBATION_FILES_KEY, out var value) || value is null
? null
: value;
#region appsettings.json
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json");
@@ -87,19 +92,11 @@ namespace AppScaffolding
System.Threading.Thread.Sleep(100);
}
#endregion
#region Settings.json
public const string LIBATION_FILES_KEY = "LibationFiles";
private const string SETTINGS_JSON = "Settings.json";
public static string SettingsJsonPath
{
get
{
var success = APPSETTINGS_TryGet(LIBATION_FILES_KEY, out var value);
return !success || value is null ? null : Path.Combine(value, SETTINGS_JSON);
}
}
public static string SettingsJsonPath => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, SETTINGS_JSON);
public static bool SettingsJson_Exists => SettingsJsonPath is not null && File.Exists(SettingsJsonPath);
public static bool Settings_TryGet(string key, out string value)
@@ -267,5 +264,10 @@ namespace AppScaffolding
System.Threading.Thread.Sleep(100);
}
#endregion
#region LibationContext.db
public const string LIBATION_CONTEXT = "LibationContext.db";
public static string DatabaseFile => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LIBATION_CONTEXT);
public static bool DatabaseFile_Exists => DatabaseFile is not null && File.Exists(DatabaseFile);
#endregion
}
}

View File

@@ -0,0 +1,6 @@
using System;
namespace AppScaffolding
{
public record UpgradeProperties(string ZipUrl, string HtmlUrl, string ZipName, Version LatestRelease);
}

View File

@@ -12,10 +12,10 @@ namespace ApplicationServices
=> LibationContext.Create(SqliteStorage.ConnectionString);
/// <summary>Use for full library querying. No lazy loading</summary>
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
public static List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)
{
using var context = GetContext();
return context.GetLibrary_Flat_NoTracking();
return context.GetLibrary_Flat_NoTracking(includeParents);
}
}
}

View File

@@ -33,18 +33,20 @@ namespace ApplicationServices
//These are the minimum response groups required for the
//library scanner to pass all validation and filtering.
var libraryResponseGroups =
LibraryOptions.ResponseGroupOptions.ProductAttrs |
LibraryOptions.ResponseGroupOptions.ProductDesc |
LibraryOptions.ResponseGroupOptions.Relationships;
if (accounts is null || accounts.Length == 0)
var libraryOptions = new LibraryOptions
{
ResponseGroups
= LibraryOptions.ResponseGroupOptions.ProductAttrs
| LibraryOptions.ResponseGroupOptions.ProductDesc
| LibraryOptions.ResponseGroupOptions.Relationships
};
if (accounts is null || accounts.Length == 0)
return new List<LibraryBook>();
try
{
logTime($"pre {nameof(scanAccountsAsync)} all");
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryResponseGroups);
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = libraryItems.Count;
@@ -102,7 +104,12 @@ namespace ApplicationServices
}
logTime($"pre {nameof(scanAccountsAsync)} all");
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
var libraryOptions = new LibraryOptions
{
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS,
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
};
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = importItems.Count;
@@ -150,7 +157,7 @@ namespace ApplicationServices
}
}
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions.ResponseGroupOptions libraryResponseGroups)
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
{
var tasks = new List<Task<List<ImportItem>>>();
foreach (var account in accounts)
@@ -159,7 +166,7 @@ namespace ApplicationServices
var apiExtended = await apiExtendedfunc(account);
// add scanAccountAsync as a TASK: do not await
tasks.Add(scanAccountAsync(apiExtended, account, libraryResponseGroups));
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions));
}
// import library in parallel
@@ -168,7 +175,7 @@ namespace ApplicationServices
return importItems;
}
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions.ResponseGroupOptions libraryResponseGroups)
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
@@ -179,7 +186,7 @@ namespace ApplicationServices
logTime($"pre scanAccountAsync {account.AccountName}");
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryResponseGroups, Configuration.Instance.ImportEpisodes);
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
@@ -193,7 +200,7 @@ namespace ApplicationServices
var libraryBookImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()");
int qtyChanges = saveChanges(context);
int qtyChanges = SaveContext(context);
logTime("importIntoDbAsync -- post SaveChanges");
// this is any changes at all to the database, not just new books
@@ -204,7 +211,7 @@ namespace ApplicationServices
return newCount;
}
private static int saveChanges(LibationContext context)
public static int SaveContext(LibationContext context)
{
try
{
@@ -246,43 +253,78 @@ namespace ApplicationServices
#endregion
// call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange()
{
SearchEngineCommands.FullReIndex();
LibrarySizeChanged?.Invoke(null, null);
}
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
/// <summary>Occurs when books are added or removed from library</summary>
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
public static event EventHandler LibrarySizeChanged;
/// <summary>
/// Occurs when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/>
/// changed values are successfully persisted.
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
/// </summary>
public static event EventHandler<string> BookUserDefinedItemCommitted;
public static event EventHandler<IEnumerable<Book>> BookUserDefinedItemCommitted;
#region Update book details
public static int UpdateUserDefinedItem(Book book)
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
{
book.UserDefinedItem.BookStatus = bookStatus;
return UpdateUserDefinedItem(book);
}
public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
{
book.UserDefinedItem.PdfStatus = pdfStatus;
return UpdateUserDefinedItem(book);
}
public static int UpdateBook(
this Book book,
string tags = null,
LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null)
=> UpdateBooks(tags, bookStatus, pdfStatus, book);
public static int UpdateBooks(
string tags = null,
LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null,
params Book[] books)
{
foreach (var book in books)
{
// blank tags are expected. null tags are not
if (tags is not null && book.UserDefinedItem.Tags != tags)
book.UserDefinedItem.Tags = tags;
if (bookStatus is not null && book.UserDefinedItem.BookStatus != bookStatus.Value)
book.UserDefinedItem.BookStatus = bookStatus.Value;
// even though PdfStatus is nullable, there's no case where we'd actually overwrite with null
if (pdfStatus is not null && book.UserDefinedItem.PdfStatus != pdfStatus.Value)
book.UserDefinedItem.PdfStatus = pdfStatus.Value;
}
return UpdateUserDefinedItem(books);
}
public static int UpdateUserDefinedItem(params Book[] books) => UpdateUserDefinedItem(books.ToList());
public static int UpdateUserDefinedItem(IEnumerable<Book> books)
{
try
{
if (books is null || !books.Any())
return 0;
using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges()
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
foreach (var book in books)
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
{
SearchEngineCommands.UpdateLiberatedStatus(book);
SearchEngineCommands.UpdateBookTags(book);
BookUserDefinedItemCommitted?.Invoke(null, book.AudibleProductId);
}
BookUserDefinedItemCommitted?.Invoke(null, books);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, $"Error updating {nameof(book.UserDefinedItem)}");
Log.Logger.Error(ex, $"Error updating {nameof(Book.UserDefinedItem)}");
throw;
}
}
@@ -290,7 +332,7 @@ namespace ApplicationServices
// must be here instead of in db layer due to AaxcExists
public static LiberatedStatus Liberated_Status(Book book)
=> book.Audio_Exists ? book.UserDefinedItem.BookStatus
=> book.Audio_Exists() ? book.UserDefinedItem.BookStatus
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
: LiberatedStatus.NotLiberated;
@@ -299,7 +341,14 @@ namespace ApplicationServices
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded) { }
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded)
{
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
public bool HasPendingBooks => PendingBooks > 0;
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError);
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded);
}
public static LibraryStats GetCounts()
{
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
@@ -317,7 +366,7 @@ namespace ApplicationServices
var boolResults = libraryBooks
.AsParallel()
.Where(lb => lb.Book.HasPdf)
.Where(lb => lb.Book.HasPdf())
.Select(lb => Pdf_Status(lb.Book))
.ToList();
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);

View File

@@ -111,13 +111,13 @@ namespace ApplicationServices
AudibleProductId = a.Book.AudibleProductId,
Locale = a.Book.Locale,
Title = a.Book.Title,
AuthorNames = a.Book.AuthorNames,
NarratorNames = a.Book.NarratorNames,
AuthorNames = a.Book.AuthorNames(),
NarratorNames = a.Book.NarratorNames(),
LengthInMinutes = a.Book.LengthInMinutes,
Description = a.Book.Description,
Publisher = a.Book.Publisher,
HasPdf = a.Book.HasPdf,
SeriesNames = a.Book.SeriesNames,
HasPdf = a.Book.HasPdf(),
SeriesNames = a.Book.SeriesNames(),
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
CommunityRatingOverall = a.Book.Rating?.OverallRating,
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating,
@@ -125,7 +125,7 @@ namespace ApplicationServices
PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.CategoriesNames.Any() ? a.Book.CategoriesNames.Aggregate((a, b) => $"{a}, {b}") : "",
CategoriesNames = a.Book.CategoriesNames().Any() ? a.Book.CategoriesNames().Aggregate((a, b) => $"{a}, {b}") : "",
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DataLayer;
using LibationSearchEngine;
@@ -7,51 +9,99 @@ namespace ApplicationServices
{
public static class SearchEngineCommands
{
public static void FullReIndex(SearchEngine engine = null)
{
engine ??= new SearchEngine();
var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library);
}
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
#region Search
public static SearchResultSet Search(string searchString) => performSafeQuery(e =>
e.Search(searchString)
);
public static void UpdateBookTags(Book book) => performSearchEngineAction_safe(e =>
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
private static T performSafeQuery<T>(Func<SearchEngine, T> func)
{
var engine = new SearchEngine();
try
{
return func(engine);
}
catch (FileNotFoundException)
{
fullReIndex(engine);
return func(engine);
}
}
#endregion
public static event EventHandler SearchEngineUpdated;
#region Update
private static bool isUpdating;
public static void UpdateBooks(IEnumerable<Book> books)
{
// Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
// I did not benchmark before choosing the number here
if (books.Count() > 15)
FullReIndex();
else
{
foreach (var book in books)
{
UpdateLiberatedStatus(book);
UpdateBookTags(book);
}
}
}
public static void FullReIndex() => performSafeCommand(e =>
fullReIndex(e)
);
public static void UpdateLiberatedStatus(Book book) => performSearchEngineAction_safe(e =>
internal static void UpdateLiberatedStatus(Book book) => performSafeCommand(e =>
e.UpdateLiberatedStatus(book)
);
private static void performSearchEngineAction_safe(Action<SearchEngine> action)
internal static void UpdateBookTags(Book book) => performSafeCommand(e =>
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
);
private static void performSafeCommand(Action<SearchEngine> action)
{
var engine = new SearchEngine();
try
{
action(engine);
update(action);
}
catch (FileNotFoundException)
{
FullReIndex(engine);
action(engine);
fullReIndex(new SearchEngine());
update(action);
}
}
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> func)
private static void update(Action<SearchEngine> action)
{
var engine = new SearchEngine();
if (action is null)
return;
// support nesting incl recursion
var prevIsUpdating = isUpdating;
try
{
return func(engine);
isUpdating = true;
action(new SearchEngine());
if (!prevIsUpdating)
SearchEngineUpdated?.Invoke(null, null);
}
catch (FileNotFoundException)
finally
{
FullReIndex(engine);
return func(engine);
isUpdating = prevIsUpdating;
}
}
private static void fullReIndex(SearchEngine engine)
{
var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library);
}
#endregion
}
}

View File

@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi.Common;
using Dinah.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Polly;
using Polly.Retry;
@@ -106,43 +110,50 @@ namespace AudibleUtilities
// 2 retries == 3 total
.RetryAsync(2);
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions.ResponseGroupOptions responseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS, bool importEpisodes = true)
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions libraryOptions, bool importEpisodes = true)
{
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or not tokens are refreshed
// worse, this 1st dummy call doesn't seem to help:
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
return policy.ExecuteAsync(() => getItemsAsync(responseGroups, importEpisodes));
return policy.ExecuteAsync(() => getItemsAsync(libraryOptions, importEpisodes));
}
private async Task<List<Item>> getItemsAsync(LibraryOptions.ResponseGroupOptions responseGroups, bool importEpisodes)
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
{
var items = new List<Item>();
#if DEBUG
//// this will not work for multi accounts
//var library_json = "library.json";
//library_json = System.IO.Path.GetFullPath(library_json);
//if (System.IO.File.Exists(library_json))
//{
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
//}
#endif
Serilog.Log.Logger.Debug("Begin initial library scan");
Serilog.Log.Logger.Debug("Beginning library scan.");
if (!items.Any())
items = await Api.GetAllLibraryItemsAsync(responseGroups);
List<Task<List<Item>>> getChildEpisodesTasks = new();
Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan");
int count = 0, maxConcurrentEpisodeScans = 5;
using SemaphoreSlim concurrencySemaphore = new(maxConcurrentEpisodeScans);
await manageEpisodesAsync(items, importEpisodes);
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions))
{
if ((item.IsEpisodes || item.IsSeriesParent) && importEpisodes)
{
//Get child episodes asynchronously and await all at the end
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
}
else if (!item.IsEpisodes)
items.Add(item);
Serilog.Log.Logger.Debug("Episode scan complete");
count++;
}
Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on {getChildEpisodesTasksCount} series episode scans to complete.", count, getChildEpisodesTasks.Count);
//await and add all episides from all parents
foreach (var epList in await Task.WhenAll(getChildEpisodesTasks))
items.AddRange(epList);
Serilog.Log.Logger.Debug("Completed library scan.");
#if DEBUG
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
#endif
var validators = new List<IValidator>();
validators.AddRange(getValidators());
foreach (var v in validators)
@@ -156,56 +167,75 @@ namespace AudibleUtilities
}
#region episodes and podcasts
private async Task manageEpisodesAsync(List<Item> items, bool importEpisodes)
private async Task<List<Item>> getChildEpisodesAsync(SemaphoreSlim concurrencySemaphore, Item parent)
{
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
await concurrencySemaphore.WaitAsync();
try
{
// get parents
var parents = items.Where(i => i.IsEpisodes).ToList();
#if DEBUG
//var parentsDebug = parents.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText("parents.json", parentsDebug);
#endif
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
if (!parents.Any())
return;
List<Item> children;
Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found");
// remove episode parents. even if the following stuff fails, these will still be removed from the collection
items.RemoveAll(i => i.IsEpisodes);
if (importEpisodes)
if (parent.IsEpisodes)
{
// add children
var children = await getEpisodesAsync(parents);
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
items.AddRange(children);
//The 'parent' is a single episode that was added to the library.
//Get the episode's parent and add it to the database.
Serilog.Log.Logger.Debug("Supplied Parent is an episode. Beginning parent scan for {parent}", parent);
children = new() { parent };
var parentAsins = parent.Relationships
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
.Select(p => p.Asin);
var seriesParents = await Api.GetCatalogProductsAsync(parentAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
int numSeriesParents = seriesParents.Count(p => p.IsSeriesParent);
if (numSeriesParents != 1)
{
//There should only ever be 1 top-level parent per episode. If not, log
//and throw so we can figure out what to do about those special cases.
JsonSerializerSettings Settings = new()
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
var ex = new ApplicationException($"Found {numSeriesParents} parents for {parent.Asin}");
Serilog.Log.Logger.Error(ex, $"Episode Product:\r\n{JsonConvert.SerializeObject(parent, Formatting.None, Settings)}");
throw ex;
}
var realParent = seriesParents.Single(p => p.IsSeriesParent);
realParent.PurchaseDate = parent.PurchaseDate;
Serilog.Log.Logger.Debug("Completed parent scan for {parent}", parent);
parent = realParent;
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding podcasts and episodes");
}
}
private async Task<List<Item>> getEpisodesAsync(List<Item> parents)
{
var results = new List<Item>();
foreach (var parent in parents)
{
var children = await getEpisodeChildrenAsync(parent);
// actual individual episode, not the parent of a series.
// for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic
if (!children.Any())
else
{
results.Add(parent);
continue;
children = await getEpisodeChildrenAsync(parent);
if (!children.Any())
return new();
}
//A series parent will always have exactly 1 Series
parent.Series = new Series[]
{
new Series
{
Asin = parent.Asin,
Sequence = "-1",
Title = parent.TitleWithSubtitle
}
};
foreach (var child in children)
{
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
@@ -217,25 +247,22 @@ namespace AudibleUtilities
{
Asin = parent.Asin,
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin).Sort.ToString(),
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0",
Title = parent.TitleWithSubtitle
}
};
// overload (read: abuse) IsEpisodes flag
child.Relationships = new Relationship[]
{
new Relationship
{
RelationshipToProduct = RelationshipToProduct.Child,
RelationshipType = RelationshipType.Episode
}
};
}
results.AddRange(children);
}
children.Add(parent);
return results;
Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent);
return children;
}
finally
{
concurrencySemaphore.Release();
}
}
private async Task<List<Item>> getEpisodeChildrenAsync(Item parent)
@@ -261,8 +288,8 @@ namespace AudibleUtilities
{
childrenBatch = await Api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
#if DEBUG
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
#endif
}
catch (Exception ex)
@@ -277,7 +304,7 @@ namespace AudibleUtilities
throw;
}
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results");
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results\t({{parent}})", parent);
// the service returned no results. probably indicates an error. stop running batches
if (!childrenBatch.Any())
break;
@@ -295,7 +322,7 @@ namespace AudibleUtilities
if (childrenIds.Count != results.Count)
{
var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}");
Serilog.Log.Logger.Error(ex, "Quantity of series episodes defined by parent does not match quantity returned by batch fetching.");
Serilog.Log.Logger.Error(ex, "{parent} - Quantity of series episodes defined by parent does not match quantity returned by batch fetching.", parent);
throw ex;
}

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="2.7.6.1" />
<PackageReference Include="AudibleApi" Version="3.1.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,13 +12,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.0.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.4">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -29,7 +29,7 @@
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<None Update="migrate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

View File

@@ -16,8 +16,14 @@ namespace DataLayer
}
}
// enum will be easier than bool to extend later
public enum ContentType { Unknown = 0, Product = 1, Episode = 2 }
// enum will be easier than bool to extend later.
public enum ContentType
{
Unknown = 0,
Product = 1,
Episode = 2,
Parent = 4,
}
public class Book
{
@@ -34,6 +40,7 @@ namespace DataLayer
// mutable
public string PictureId { get; set; }
public string PictureLarge { get; set; }
// book details
public bool IsAbridged { get; private set; }
@@ -42,27 +49,10 @@ namespace DataLayer
// non-null. use "empty pattern"
internal int CategoryId { get; private set; }
public Category Category { get; private set; }
public string[] CategoriesNames
=> Category is null ? new string[0]
: Category.ParentCategory is null ? new[] { Category.Name }
: new[] { Category.ParentCategory.Name, Category.Name };
public string[] CategoriesIds
=> Category is null ? null
: Category.ParentCategory is null ? new[] { Category.AudibleCategoryId }
: new[] { Category.ParentCategory.AudibleCategoryId, Category.AudibleCategoryId };
public string TitleSortable => Formatters.GetSortName(Title);
public string SeriesSortable => Formatters.GetSortName(SeriesNames);
// is owned, not optional 1:1
public UserDefinedItem UserDefinedItem { get; private set; }
// UserDefinedItem convenience properties
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
public bool Audio_Exists => UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated;
/// <summary>True if exists and IsLiberated. Else false</summary>
public bool PDF_Exists => UserDefinedItem.PdfStatus == LiberatedStatus.Liberated;
// is owned, not optional 1:1
/// <summary>The product's aggregate community rating</summary>
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
@@ -124,11 +114,7 @@ namespace DataLayer
.ToList();
public IEnumerable<Contributor> Authors => getContributions(Role.Author).Select(bc => bc.Contributor).ToList();
public string AuthorNames => string.Join(", ", Authors.Select(a => a.Name));
public IEnumerable<Contributor> Narrators => getContributions(Role.Narrator).Select(bc => bc.Contributor).ToList();
public string NarratorNames => string.Join(", ", Narrators.Select(n => n.Name));
public string Publisher => getContributions(Role.Publisher).SingleOrDefault()?.Contributor.Name;
public void ReplaceAuthors(IEnumerable<Contributor> authors, DbContext context = null)
@@ -184,30 +170,6 @@ namespace DataLayer
#region series
private HashSet<SeriesBook> _seriesLink;
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList();
public string SeriesNames
{
get
{
if (_seriesLink is null)
return "";
// first: alphabetical by name
var withNames = _seriesLink
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.Name)
.OrderBy(a => a)
.ToList();
// then un-named are alpha by series id
var nullNames = _seriesLink
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.AudibleSeriesId)
.OrderBy(a => a)
.ToList();
var all = withNames.Union(nullNames).ToList();
return string.Join(", ", all);
}
}
public void UpsertSeries(Series series, string order, DbContext context = null)
{
@@ -229,7 +191,6 @@ namespace DataLayer
#region supplements
private HashSet<Supplement> _supplements;
public IEnumerable<Supplement> Supplements => _supplements?.ToList();
public bool HasPdf => Supplements.Any();
public void AddSupplementDownloadUrl(string url)
{

View File

@@ -12,7 +12,7 @@ namespace DataLayer
public float StoryRating { get; private set; }
private Rating() { }
internal Rating(float overallRating, float performanceRating, float storyRating)
public Rating(float overallRating, float performanceRating, float storyRating)
{
OverallRating = overallRating;
PerformanceRating = performanceRating;
@@ -38,41 +38,6 @@ namespace DataLayer
yield return StoryRating;
}
public float FirstScore
=> OverallRating > 0 ? OverallRating
: PerformanceRating > 0 ? PerformanceRating
: StoryRating;
/// <summary>character: ★</summary>
const char STAR = '\u2605';
/// <summary>character: ½</summary>
const char HALF = '\u00BD';
string getStars(float score)
{
var fullStars = (int)Math.Floor(score);
var starString = "".PadLeft(fullStars, STAR);
if (score - fullStars == 0.5f)
starString += HALF;
return starString;
}
public string ToStarString()
{
var items = new List<string>();
if (OverallRating > 0)
items.Add($"Overall: {getStars(OverallRating)}");
if (PerformanceRating > 0)
items.Add($"Perform: {getStars(PerformanceRating)}");
if (StoryRating > 0)
items.Add($"Story: {getStars(StoryRating)}");
return string.Join("\r\n", items);
}
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
}
}

View File

@@ -141,9 +141,11 @@ namespace DataLayer
get => _bookStatus;
set
{
if (_bookStatus != value)
{
_bookStatus = value;
// PartialDownload is a live/ephemeral status, not a persistent one. Do not store
var displayStatus = value == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : value;
if (_bookStatus != displayStatus)
{
_bookStatus = displayStatus;
OnItemChanged(nameof(BookStatus));
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataLayer
{
public static class EntityExtensions
{
public static string TitleSortable(this Book book) => Formatters.GetSortName(book.Title);
public static string AuthorNames(this Book book) => string.Join(", ", book.Authors.Select(a => a.Name));
public static string NarratorNames(this Book book) => string.Join(", ", book.Narrators.Select(n => n.Name));
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
public static bool Audio_Exists(this Book book) => book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated;
/// <summary>True if exists and IsLiberated. Else false</summary>
public static bool PDF_Exists(this Book book) => book.UserDefinedItem.PdfStatus == LiberatedStatus.Liberated;
public static string SeriesSortable(this Book book) => Formatters.GetSortName(book.SeriesNames());
public static bool HasPdf(this Book book) => book.Supplements.Any();
public static string SeriesNames(this Book book)
{
if (book.SeriesLink is null)
return "";
// first: alphabetical by name
var withNames = book.SeriesLink
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.Name)
.OrderBy(a => a)
.ToList();
// then un-named are alpha by series id
var nullNames = book.SeriesLink
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.AudibleSeriesId)
.OrderBy(a => a)
.ToList();
var all = withNames.Union(nullNames).ToList();
return string.Join(", ", all);
}
public static string[] CategoriesNames(this Book book)
=> book.Category is null ? new string[0]
: book.Category.ParentCategory is null ? new[] { book.Category.Name }
: new[] { book.Category.ParentCategory.Name, book.Category.Name };
public static string[] CategoriesIds(this Book book)
=> book.Category is null ? null
: book.Category.ParentCategory is null ? new[] { book.Category.AudibleCategoryId }
: new[] { book.Category.ParentCategory.AudibleCategoryId, book.Category.AudibleCategoryId };
public static string AggregateTitles(this IEnumerable<LibraryBook> libraryBooks, int max = 5)
{
if (libraryBooks is null || !libraryBooks.Any())
return "";
max = Math.Max(max, 1);
var titles = libraryBooks.Select(lb => "- " + lb.Book.Title).ToList();
var titlesAgg = titles.Take(max).Aggregate((a, b) => $"{a}\r\n{b}");
if (titles.Count == max + 1)
titlesAgg += $"\r\n\r\nand 1 other";
else if (titles.Count > max + 1)
titlesAgg += $"\r\n\r\nand {titles.Count - max } others";
return titlesAgg;
}
public static float FirstScore(this Rating rating)
=> rating.OverallRating > 0 ? rating.OverallRating
: rating.PerformanceRating > 0 ? rating.PerformanceRating
: rating.StoryRating;
public static string ToStarString(this Rating rating)
{
var items = new List<string>();
if (rating.OverallRating > 0)
items.Add($"Overall: {getStars(rating.OverallRating)}");
if (rating.PerformanceRating > 0)
items.Add($"Perform: {getStars(rating.PerformanceRating)}");
if (rating.StoryRating > 0)
items.Add($"Story: {getStars(rating.StoryRating)}");
return string.Join("\r\n", items);
}
/// <summary>character: ★</summary>
const char STAR = '\u2605';
/// <summary>character: ½</summary>
const char HALF = '\u00BD';
private static string getStars(float score)
{
var fullStars = (int)Math.Floor(score);
var starString = new string(STAR, fullStars);
if (score - fullStars >= 0.25f)
starString += HALF;
return starString;
}
}
}

View File

@@ -10,7 +10,7 @@ namespace DataLayer
// ========================
// these run against the db. linq queries against these MUST be translatable to sql. primatives only. no POCOs or this error occurs:
// Unable to create a constant value of type 'DataLayer.Contributor'. Only primitive types or enumeration types are supported in this context.
// to use full object-linq, load and use local
// to use full object-linq, load and use Local. HOWEVER, Local is only hashed/indexed on PK. All other searches are very slow
// load full table:
// List<Contributor> contributors = ...;
// Contributors.Load();

View File

Binary file not shown.

View File

@@ -0,0 +1,394 @@
// <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("20220510175257_AddPictureIDLargeMigration")]
partial class AddPictureIDLargeMigration
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.4");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
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<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>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
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.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
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<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
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("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
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<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("Category");
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
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.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
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("ContributorsLink");
b.Navigation("SeriesLink");
});
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,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DataLayer.Migrations
{
public partial class AddPictureIDLargeMigration : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PictureLarge",
table: "Books",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PictureLarge",
table: "Books");
}
}
}

View File

@@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
@@ -13,8 +15,7 @@ namespace DataLayer.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.10");
modelBuilder.HasAnnotation("ProductVersion", "6.0.4");
modelBuilder.Entity("DataLayer.Book", b =>
{
@@ -49,6 +50,9 @@ namespace DataLayer.Migrations
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("PictureLarge")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
@@ -267,7 +271,7 @@ namespace DataLayer.Migrations
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.ToTable("UserDefinedItem", (string)null);
b1.WithOwner("Book")
.HasForeignKey("BookId");

View File

@@ -35,5 +35,15 @@ namespace DataLayer
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(b => b.Category).ThenInclude(c => c.ParentCategory);
public static bool IsProduct(this Book book)
=> book.ContentType is not ContentType.Episode and not ContentType.Parent;
public static bool IsEpisodeChild(this Book book)
=> book.ContentType is ContentType.Episode;
public static bool IsEpisodeParent(this Book book)
=> book.ContentType is ContentType.Parent;
}
}

View File

@@ -15,11 +15,13 @@ namespace DataLayer
// .GetLibrary()
// .ToList();
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context)
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context, bool includeParents = false)
=> context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
.GetLibrary()
.AsEnumerable()
.Where(lb => !lb.Book.IsEpisodeParent() || includeParents)
.ToList();
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
@@ -40,5 +42,32 @@ namespace DataLayer
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory);
#nullable enable
public static LibraryBook? FindSeriesParent(this IEnumerable<LibraryBook> libraryBooks, LibraryBook seriesEpisode)
{
if (seriesEpisode.Book.SeriesLink is null) return null;
//Parent books will always have exactly 1 SeriesBook due to how
//they are imported in ApiExtended.getChildEpisodesAsync()
return libraryBooks.FirstOrDefault(
lb =>
lb.Book.IsEpisodeParent() &&
seriesEpisode.Book.SeriesLink.Any(
s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId));
}
#nullable disable
public static IEnumerable<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
=> bookList
.Where(
lb =>
lb.Book.IsEpisodeChild() &&
lb.Book.SeriesLink?
.Any(
s =>
s.Series.AudibleSeriesId == parent.Book.AudibleProductId
) == true
).ToList();
}
}

View File

@@ -1,6 +0,0 @@
{
"ConnectionStrings": {
"// this connection string is ONLY used for DataLayer's Migrations. this appsettings.json file is NOT used at all by application; it is overwritten": "",
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;"
}
}

View File

@@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;"
}
}

View File

@@ -75,7 +75,7 @@ namespace DtoImporterService
{
var item = importItem.DtoItem;
var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product;
var contentType = GetContentType(item);
// absence of authors is very rare, but possible
if (!item.Authors?.Any() ?? true)
@@ -164,6 +164,9 @@ namespace DtoImporterService
// set/update book-specific info which may have changed
if (item.PictureId is not null)
book.PictureId = item.PictureId;
if (item.PictureLarge is not null)
book.PictureLarge = item.PictureLarge;
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
@@ -181,5 +184,15 @@ namespace DtoImporterService
}
}
}
private static DataLayer.ContentType GetContentType(Item item)
{
if (item.IsEpisodes)
return DataLayer.ContentType.Episode;
else if (item.IsSeriesParent)
return DataLayer.ContentType.Parent;
else
return DataLayer.ContentType.Product;
}
}
}

View File

@@ -4,7 +4,8 @@ namespace FileLiberator
{
public abstract class AudioDecodable : Processable
{
public event EventHandler<Action<byte[]>> RequestCoverArt;
public delegate byte[] RequestCoverArtHandler(object sender, EventArgs eventArgs);
public event RequestCoverArtHandler RequestCoverArt;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
@@ -32,10 +33,10 @@ namespace FileLiberator
NarratorsDiscovered?.Invoke(this, narrators);
}
protected void OnRequestCoverArt(Action<byte[]> setCoverArtDel)
protected byte[] OnRequestCoverArt()
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
RequestCoverArt?.Invoke(this, setCoverArtDel);
return RequestCoverArt?.Invoke(this, new());
}
protected void OnCoverImageDiscovered(byte[] coverImage)

View File

@@ -14,19 +14,27 @@ namespace FileLiberator
{
public class ConvertToMp3 : AudioDecodable
{
public override string Name => "Convert to Mp3";
private Mp4File m4bBook;
private long fileSize;
private long fileSize;
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
public override void Cancel() => m4bBook?.Cancel();
private bool cancelled = false;
public override void Cancel()
{
m4bBook?.Cancel();
cancelled = true;
}
public override bool Validate(LibraryBook libraryBook)
{
public static bool ValidateMp3(LibraryBook libraryBook)
{
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
}
public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
OnBegin(libraryBook);
@@ -56,12 +64,12 @@ namespace FileLiberator
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
OnFileCreated(libraryBook, realMp3Path);
var statusHandler = new StatusHandler();
if (result == ConversionResult.Failed)
statusHandler.AddError("Conversion failed");
return statusHandler;
return new StatusHandler { "Conversion failed" };
else if (result == ConversionResult.Cancelled)
return new StatusHandler { "Cancelled" };
else
return new StatusHandler();
}
finally
{

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AaxDecrypter;
using ApplicationServices;
using AudibleApi;
using DataLayer;
using Dinah.Core;
@@ -15,9 +16,10 @@ namespace FileLiberator
{
public class DownloadDecryptBook : AudioDecodable
{
public override string Name => "Download & Decrypt";
private AudiobookDownloadBase abDownloader;
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
public override void Cancel() => abDownloader?.Cancel();
@@ -42,7 +44,7 @@ namespace FileLiberator
try
{
if (libraryBook.Book.Audio_Exists)
if (libraryBook.Book.Audio_Exists())
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
bool success = false;
@@ -65,17 +67,23 @@ namespace FileLiberator
foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC))
FileUtility.SaferDelete(tmpFile.Path);
return new StatusHandler { "Decrypt failed" };
return abDownloader?.IsCanceled == true ?
new StatusHandler { "Cancelled" } :
new StatusHandler { "Decrypt failed" };
}
// moves new files from temp dir to final dest
var movedAudioFile = moveFilesToBooksDir(libraryBook, entries);
// moves new files from temp dir to final dest.
// This could take a few seconds if moving hundreds of files.
var movedAudioFile = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
// decrypt failed
if (!movedAudioFile)
return new StatusHandler { "Cannot find final audio file after decryption" };
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
if (Configuration.Instance.DownloadCoverArt)
DownloadCoverArt(libraryBook);
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
return new StatusHandler();
}
@@ -113,7 +121,7 @@ namespace FileLiberator
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic);
if (config.AllowLibationFixup)
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames);
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames());
abDownloader = converter;
}
@@ -128,6 +136,7 @@ namespace FileLiberator
// REAL WORK DONE HERE
var success = await Task.Run(abDownloader.Run);
return success;
}
finally
@@ -187,31 +196,27 @@ namespace FileLiberator
}
}
NAudio.Lame.LameConfig lameConfig = new();
lameConfig.Mode = NAudio.Lame.MPEGMode.Mono;
audiobookDlLic.LameConfig = new();
audiobookDlLic.LameConfig.Mode = NAudio.Lame.MPEGMode.Mono;
if (config.LameTargetBitrate)
{
if (config.LameConstantBitrate)
lameConfig.BitRate = config.LameBitrate;
audiobookDlLic.LameConfig.BitRate = config.LameBitrate;
else
{
lameConfig.ABRRateKbps = config.LameBitrate;
lameConfig.VBR = NAudio.Lame.VBRMode.ABR;
lameConfig.WriteVBRTag = true;
audiobookDlLic.LameConfig.ABRRateKbps = config.LameBitrate;
audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.ABR;
audiobookDlLic.LameConfig.WriteVBRTag = true;
}
}
else
{
lameConfig.VBR = NAudio.Lame.VBRMode.Default;
lameConfig.VBRQuality = config.LameVBRQuality;
lameConfig.WriteVBRTag = true;
audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.Default;
audiobookDlLic.LameConfig.VBRQuality = config.LameVBRQuality;
audiobookDlLic.LameConfig.WriteVBRTag = true;
}
audiobookDlLic.LameConfig = lameConfig;
return audiobookDlLic;
}
@@ -242,7 +247,7 @@ namespace FileLiberator
if (e is not null)
OnCoverImageDiscovered(e);
else if (Configuration.Instance.AllowLibationFixup)
OnRequestCoverArt(abDownloader.SetCoverArt);
abDownloader.SetCoverArt(OnRequestCoverArt());
}
/// <summary>Move new files to 'Books' directory</summary>
@@ -277,5 +282,34 @@ namespace FileLiberator
return true;
}
private void DownloadCoverArt(LibraryBook libraryBook)
{
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
var coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
try
{
if (File.Exists(coverPath))
FileUtility.SaferDelete(coverPath);
(string picId, PictureSize size) = libraryBook.Book.PictureLarge is null ?
(libraryBook.Book.PictureId, PictureSize.Native) :
(libraryBook.Book.PictureLarge, PictureSize.Native);
var picBytes = PictureStorage.GetPictureSynchronously(new PictureDefinition(picId, size));
if (picBytes.Length > 0)
File.WriteAllBytes(coverPath, picBytes);
}
catch (Exception ex)
{
//Failure to download cover art should not be
//considered a failure to download the book
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
}
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Dinah.Core.Net.Http;
namespace FileLiberator
{
// currently only used to download the .zip flies for upgrade
public class DownloadFile : Streamable
{
public async Task<string> PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath)
{
var client = new HttpClient();
var progress = new Progress<DownloadProgress>(OnStreamingProgressChanged);
OnStreamingBegin(proposedDownloadFilePath);
try
{
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
OnFileCreated("Upgrade", actualDownloadedFilePath);
return actualDownloadedFilePath;
}
finally
{
OnStreamingCompleted(proposedDownloadFilePath);
}
}
}
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using ApplicationServices;
using DataLayer;
using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
@@ -14,9 +15,10 @@ namespace FileLiberator
{
public class DownloadPdf : Processable
{
public override string Name => "Download Pdf";
public override bool Validate(LibraryBook libraryBook)
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !libraryBook.Book.PDF_Exists;
&& !libraryBook.Book.PDF_Exists();
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
@@ -28,7 +30,7 @@ namespace FileLiberator
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(actualDownloadedFilePath);
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
libraryBook.Book.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
return result;
}

View File

@@ -6,6 +6,7 @@
<ItemGroup>
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>

View File

@@ -5,11 +5,13 @@ using System.Threading.Tasks;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using LibationFileManager;
namespace FileLiberator
{
public abstract class Processable : Streamable
{
public abstract string Name { get; }
public event EventHandler<LibraryBook> Begin;
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
@@ -27,7 +29,7 @@ namespace FileLiberator
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
=> library.Where(libraryBook =>
Validate(libraryBook)
&& (libraryBook.Book.ContentType != ContentType.Episode || LibationFileManager.Configuration.Instance.DownloadEpisodes)
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.Instance.DownloadEpisodes)
);
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
@@ -49,7 +51,7 @@ namespace FileLiberator
return status;
}
public async Task<StatusHandler> TryProcessAsync(LibraryBook libraryBook)
=> Validate(libraryBook)
? await ProcessAsync(libraryBook)

View File

@@ -134,6 +134,8 @@ namespace FileManager
private void AddPath(string path)
{
if (!File.Exists(path) && !Directory.Exists(path))
return;
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));
else

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="4.2.0.1" />
<PackageReference Include="Dinah.Core" Version="4.4.0.1" />
<PackageReference Include="Polly" Version="7.2.3" />
</ItemGroup>

View File

@@ -0,0 +1,18 @@
using AppScaffolding;
namespace Hangover
{
public partial class Form1
{
private void Load_cliTab()
{
}
private void cliTab_VisibleChanged(object sender, EventArgs e)
{
if (!databaseTab.Visible)
return;
}
}
}

View File

@@ -0,0 +1,140 @@
using ApplicationServices;
using AppScaffolding;
using Microsoft.EntityFrameworkCore;
namespace Hangover
{
public partial class Form1
{
private string dbFile;
private void Load_databaseTab()
{
dbFile = UNSAFE_MigrationHelper.DatabaseFile;
if (dbFile is null)
{
databaseFileLbl.Text = $"Database file not found";
return;
}
databaseFileLbl.Text = $"Database file: {UNSAFE_MigrationHelper.DatabaseFile ?? "not found"}";
}
private void databaseTab_VisibleChanged(object sender, EventArgs e)
{
if (!databaseTab.Visible)
return;
}
private void sqlExecuteBtn_Click(object sender, EventArgs e)
{
ensureBackup();
sqlResultsTb.Clear();
try
{
var sql = sqlTb.Text.Trim();
#region // explanation
// Routing statements to non-query is a convenience.
// I went down the rabbit hole of full parsing and it's more trouble than it's worth. The parsing is easy due to available libraries. The edge cases of what to do next got too complex for slight gains.
// It's also not useful to take the extra effort to separate non-queries which don't return a row count. Eg: alter table, drop table
// My half-assed solution here won't even catch simple mistakes like this -- and that's ok
// -- line 1 is a comment
// delete from foo
#endregion
var lower = sql.ToLower();
if (lower.StartsWith("update") || lower.StartsWith("insert") || lower.StartsWith("delete"))
nonQuery(sql);
else
query(sql);
}
catch (Exception ex)
{
sqlResultsTb.Text = $"{ex.Message}\r\n{ex.StackTrace}";
}
finally
{
deleteUnneededBackups();
}
}
private string dbBackup;
private DateTime dbFileLastModified;
private void ensureBackup()
{
if (dbBackup is not null)
return;
dbFileLastModified = File.GetLastWriteTimeUtc(dbFile);
dbBackup
= Path.ChangeExtension(dbFile, "").TrimEnd('.')
+ $"_backup_{DateTime.UtcNow:O}".Replace(':', '-').Replace('.', '-')
+ Path.GetExtension(dbFile);
File.Copy(dbFile, dbBackup);
}
private void deleteUnneededBackups()
{
var newLastModified = File.GetLastWriteTimeUtc(dbFile);
if (dbFileLastModified == newLastModified)
{
File.Delete(dbBackup);
dbBackup = null;
}
}
void query(string sql)
{
// ef doesn't support truly generic queries. have to drop down to ado.net
using var context = DbContexts.GetContext();
using var conn = context.Database.GetDbConnection();
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
var reader = cmd.ExecuteReader();
var results = 0;
var builder = new System.Text.StringBuilder();
var lines = 0;
while (reader.Read())
{
results++;
for (var i = 0; i < reader.FieldCount; i++)
builder.Append(reader.GetValue(i) + "\t");
builder.AppendLine();
lines++;
if (lines % 10 == 0)
{
sqlResultsTb.AppendText(builder.ToString());
builder.Clear();
}
}
sqlResultsTb.AppendText(builder.ToString());
builder.Clear();
if (results == 0)
sqlResultsTb.Text = "[no results]";
else
{
sqlResultsTb.AppendText($"\r\n{results} result");
if (results != 1) sqlResultsTb.AppendText("s");
}
}
void nonQuery(string sql)
{
using var context = DbContexts.GetContext();
var results = context.Database.ExecuteSqlRaw(sql);
sqlResultsTb.AppendText($"{results} record");
if (results != 1) sqlResultsTb.AppendText("s");
sqlResultsTb.AppendText(" affected");
}
}
}

158
Source/Hangover/Form1.Designer.cs generated Normal file
View File

@@ -0,0 +1,158 @@
namespace Hangover
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.tabControl1 = new System.Windows.Forms.TabControl();
this.databaseTab = new System.Windows.Forms.TabPage();
this.sqlExecuteBtn = new System.Windows.Forms.Button();
this.sqlResultsTb = new System.Windows.Forms.TextBox();
this.sqlTb = new System.Windows.Forms.TextBox();
this.sqlLbl = new System.Windows.Forms.Label();
this.databaseFileLbl = new System.Windows.Forms.Label();
this.cliTab = new System.Windows.Forms.TabPage();
this.tabControl1.SuspendLayout();
this.databaseTab.SuspendLayout();
this.SuspendLayout();
//
// tabControl1
//
this.tabControl1.Controls.Add(this.databaseTab);
this.tabControl1.Controls.Add(this.cliTab);
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl1.Location = new System.Drawing.Point(0, 0);
this.tabControl1.Name = "tabControl1";
this.tabControl1.SelectedIndex = 0;
this.tabControl1.Size = new System.Drawing.Size(800, 450);
this.tabControl1.TabIndex = 0;
//
// databaseTab
//
this.databaseTab.Controls.Add(this.sqlExecuteBtn);
this.databaseTab.Controls.Add(this.sqlResultsTb);
this.databaseTab.Controls.Add(this.sqlTb);
this.databaseTab.Controls.Add(this.sqlLbl);
this.databaseTab.Controls.Add(this.databaseFileLbl);
this.databaseTab.Location = new System.Drawing.Point(4, 24);
this.databaseTab.Name = "databaseTab";
this.databaseTab.Padding = new System.Windows.Forms.Padding(3);
this.databaseTab.Size = new System.Drawing.Size(792, 422);
this.databaseTab.TabIndex = 0;
this.databaseTab.Text = "Database";
this.databaseTab.UseVisualStyleBackColor = true;
//
// sqlExecuteBtn
//
this.sqlExecuteBtn.Location = new System.Drawing.Point(8, 153);
this.sqlExecuteBtn.Name = "sqlExecuteBtn";
this.sqlExecuteBtn.Size = new System.Drawing.Size(75, 23);
this.sqlExecuteBtn.TabIndex = 3;
this.sqlExecuteBtn.Text = "Execute";
this.sqlExecuteBtn.UseVisualStyleBackColor = true;
this.sqlExecuteBtn.Click += new System.EventHandler(this.sqlExecuteBtn_Click);
//
// sqlResultsTb
//
this.sqlResultsTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.sqlResultsTb.Location = new System.Drawing.Point(8, 182);
this.sqlResultsTb.Multiline = true;
this.sqlResultsTb.Name = "sqlResultsTb";
this.sqlResultsTb.ReadOnly = true;
this.sqlResultsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.sqlResultsTb.Size = new System.Drawing.Size(776, 234);
this.sqlResultsTb.TabIndex = 4;
//
// sqlTb
//
this.sqlTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.sqlTb.Location = new System.Drawing.Point(8, 48);
this.sqlTb.Multiline = true;
this.sqlTb.Name = "sqlTb";
this.sqlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.sqlTb.Size = new System.Drawing.Size(778, 99);
this.sqlTb.TabIndex = 2;
//
// sqlLbl
//
this.sqlLbl.AutoSize = true;
this.sqlLbl.Location = new System.Drawing.Point(6, 30);
this.sqlLbl.Name = "sqlLbl";
this.sqlLbl.Size = new System.Drawing.Size(144, 15);
this.sqlLbl.TabIndex = 1;
this.sqlLbl.Text = "SQL (database command)";
//
// databaseFileLbl
//
this.databaseFileLbl.AutoSize = true;
this.databaseFileLbl.Location = new System.Drawing.Point(6, 3);
this.databaseFileLbl.Name = "databaseFileLbl";
this.databaseFileLbl.Size = new System.Drawing.Size(80, 15);
this.databaseFileLbl.TabIndex = 0;
this.databaseFileLbl.Text = "Database file: ";
//
// cliTab
//
this.cliTab.Location = new System.Drawing.Point(4, 24);
this.cliTab.Name = "cliTab";
this.cliTab.Size = new System.Drawing.Size(792, 422);
this.cliTab.TabIndex = 1;
this.cliTab.Text = "Command Line Interface";
this.cliTab.UseVisualStyleBackColor = true;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Controls.Add(this.tabControl1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "Form1";
this.Text = "Hangover: Libation debug and recovery tool";
this.tabControl1.ResumeLayout(false);
this.databaseTab.ResumeLayout(false);
this.databaseTab.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private TabControl tabControl1;
private TabPage databaseTab;
private Label databaseFileLbl;
private TextBox sqlResultsTb;
private TextBox sqlTb;
private Label sqlLbl;
private Button sqlExecuteBtn;
private TabPage cliTab;
}
}

16
Source/Hangover/Form1.cs Normal file
View File

@@ -0,0 +1,16 @@
namespace Hangover
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
databaseTab.VisibleChanged += databaseTab_VisibleChanged;
cliTab.VisibleChanged += cliTab_VisibleChanged;
Load_databaseTab();
Load_cliTab();
}
}
}

2328
Source/Hangover/Form1.resx Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>hangover.ico</ApplicationIcon>
<ImplicitUsings>enable</ImplicitUsings>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<!--
When LibationWinForms and Hangover output to the same dir, Hangover must build before LibationWinForms
VS > rt-clk solution > Properties
left: Project Dependencies
top: Projects: LibationWinForms
bottom: manually check Hangover
edit debug and release output paths
-->
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\LibationWinForms\bin\Debug</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\LibationWinForms\bin\Release</OutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Form1.*.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
namespace Hangover
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

Binary file not shown.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
ProjectSection(SolutionItems) = preProject
__README - COLLABORATORS.txt = __README - COLLABORATORS.txt
__TODO.txt = __TODO.txt
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
_DB_NOTES.txt = _DB_NOTES.txt
REFERENCE.txt = REFERENCE.txt
EndProjectSection
@@ -37,6 +38,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "Lib
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "LibationWinForms\LibationWinForms.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}"
ProjectSection(ProjectDependencies) = postProject
{40C67036-C1A7-4FDF-AA83-8EC902E257F3} = {40C67036-C1A7-4FDF-AA83-8EC902E257F3}
{428163C3-D558-4914-B570-A92069521877} = {428163C3-D558-4914-B570-A92069521877}
EndProjectSection
EndProject
@@ -64,6 +66,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager.Tests", "_Tests
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager.Tests", "_Tests\LibationFileManager.Tests\LibationFileManager.Tests.csproj", "{EB781571-8548-477E-82AD-FB9FAB548D2F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangover", "Hangover\Hangover.csproj", "{40C67036-C1A7-4FDF-AA83-8EC902E257F3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -142,6 +146,10 @@ Global
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.Build.0 = Release|Any CPU
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -165,6 +173,7 @@ Global
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{40C67036-C1A7-4FDF-AA83-8EC902E257F3} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}

View File

@@ -14,11 +14,13 @@
<!--
When LibationWinForms and LibationCli output to the same dir, LibationCli must build before LibationWinForms
VS > rt-clik solution > Project Build Order...
Dependencies [tab]
Projects: LibationWinForms
manually check LibationCli
VS > rt-clk solution > Properties
left: Project Dependencies
top: Projects: LibationWinForms
bottom: manually check LibationCli
edit debug and release output paths
-->
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\LibationWinForms\bin\Debug</OutputPath>
@@ -29,11 +31,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>

View File

@@ -9,7 +9,6 @@ using FileLiberator;
namespace LibationCli
{
// streamlined, non-Forms copy of ProcessorAutomationController
public abstract class ProcessableOptionsBase : OptionsBase
{
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook> completedAction = null)

View File

@@ -27,7 +27,6 @@ namespace LibationCli
// //
//***********************************************//
Setup.Initialize();
Setup.SubscribeToDatabaseEvents();
var types = Setup.LoadVerbs();

View File

@@ -34,15 +34,15 @@ namespace LibationCli
private static void checkForUpdate()
{
var (hasUpgrade, zipUrl, htmlUrl, zipName) = LibationScaffolding.GetLatestRelease();
if (!hasUpgrade)
var upgradeProperties = LibationScaffolding.GetLatestRelease();
if (upgradeProperties is null)
return;
var origColor = Console.ForegroundColor;
try
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"UPDATE AVAILABLE @ {zipUrl}");
Console.WriteLine($"UPDATE AVAILABLE @ {upgradeProperties.ZipUrl}");
}
finally
{
@@ -50,11 +50,6 @@ namespace LibationCli
}
}
public static void SubscribeToDatabaseEvents()
{
DataLayer.UserDefinedItem.ItemChanged += (sender, e) => ApplicationServices.LibraryCommands.UpdateUserDefinedItem(((DataLayer.UserDefinedItem)sender).Book);
}
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetCustomAttribute<VerbAttribute>() is not null)

View File

@@ -179,6 +179,34 @@ namespace LibationFileManager
get => persistentDictionary.GetNonString<int>(nameof(LameVBRQuality));
set => persistentDictionary.SetNonString(nameof(LameVBRQuality), value);
}
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
public Dictionary<string, bool> GridColumnsVisibilities
{
get => persistentDictionary.GetNonString<Dictionary<string, bool>>(nameof(GridColumnsVisibilities));
set => persistentDictionary.SetNonString(nameof(GridColumnsVisibilities), value);
}
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
public Dictionary<string, int> GridColumnsDisplayIndices
{
get => persistentDictionary.GetNonString<Dictionary<string,int>>(nameof(GridColumnsDisplayIndices));
set => persistentDictionary.SetNonString(nameof(GridColumnsDisplayIndices), value);
}
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
public Dictionary<string, int> GridColumnsWidths
{
get => persistentDictionary.GetNonString<Dictionary<string,int>>(nameof(GridColumnsWidths));
set => persistentDictionary.SetNonString(nameof(GridColumnsWidths), value);
}
[Description("Save cover image alongside audiobook?")]
public bool DownloadCoverArt
{
get => persistentDictionary.GetNonString<bool>(nameof(DownloadCoverArt));
set => persistentDictionary.SetNonString(nameof(DownloadCoverArt), value);
}
public enum BadBookAction
{
@@ -240,6 +268,13 @@ namespace LibationFileManager
}
}
[Description("Auto download episodes? Efter scan, download new books in 'checked' accounts.")]
public bool AutoDownloadEpisodes
{
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes));
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
}
#region templates: custom file naming
[Description("How to format the folders in which files will be saved")]
@@ -393,7 +428,7 @@ namespace LibationFileManager
return libationFilesPathCache;
// FIRST: must write here before SettingsFilePath in next step reads cache
libationFilesPathCache = getLiberationFilesSettingFromJson();
libationFilesPathCache = getLibationFilesSettingFromJson();
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
persistentDictionary = new PersistentDictionary(SettingsFilePath);
@@ -415,7 +450,7 @@ namespace LibationFileManager
private static string libationFilesPathCache;
private string getLiberationFilesSettingFromJson()
private string getLibationFilesSettingFromJson()
{
string startingContents = null;
try
@@ -454,6 +489,14 @@ namespace LibationFileManager
{
libationFilesPathCache = null;
// ensure exists
if (!File.Exists(APPSETTINGS_JSON))
{
// getter creates new file, loads PersistentDictionary
var _ = LibationFiles;
System.Threading.Thread.Sleep(100);
}
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
var jObj = JObject.Parse(startingContents);

View File

@@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.1.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -8,13 +8,13 @@ using System.Threading.Tasks;
namespace LibationFileManager
{
public enum PictureSize { _80x80 = 80, _300x300 = 300, _500x500 = 500 }
public enum PictureSize { Native, _80x80 = 80, _300x300 = 300, _500x500 = 500 }
public class PictureCachedEventArgs : EventArgs
{
public PictureDefinition Definition { get; internal set; }
public byte[] Picture { get; internal set; }
}
public struct PictureDefinition
public struct PictureDefinition : IEquatable<PictureDefinition>
{
public string PictureId { get; }
public PictureSize Size { get; }
@@ -24,6 +24,11 @@ namespace LibationFileManager
PictureId = pictureId;
Size = pictureSize;
}
public bool Equals(PictureDefinition other)
{
return PictureId == other.PictureId && Size == other.Size;
}
}
public static class PictureStorage
{
@@ -113,8 +118,8 @@ namespace LibationFileManager
try
{
var sz = (int)def.Size;
var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}._SL{sz}_.jpg").Result;
var sizeStr = def.Size == PictureSize.Native ? "" : $"._SL{(int)def.Size}_";
var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg").Result;
// save image file. make sure to not save default image
var path = getPath(def);

View File

@@ -28,16 +28,22 @@ namespace LibationFileManager
inMemoryState = JsonConvert.DeserializeObject<FilterState>(File.ReadAllText(JsonFile));
}
public static event EventHandler UseDefaultChanged;
public static bool UseDefault
{
get => inMemoryState.UseDefault;
set
{
if (UseDefault == value)
return;
lock (locker)
{
inMemoryState.UseDefault = value;
save(false);
}
UseDefaultChanged?.Invoke(null, null);
}
}

View File

@@ -18,20 +18,20 @@ namespace LibationSearchEngine
{
public const Lucene.Net.Util.Version Version = Lucene.Net.Util.Version.LUCENE_30;
// not customizable. don't move to config
private static string SearchEngineDirectory { get; }
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("SearchEngine").FullName;
public const string _ID_ = "_ID_";
public const string TAGS = "tags";
// special field for each book which includes all major parts of the book's metadata. enables non-targetting searching
public const string ALL = "all";
// the workaround which allows displaying all books when query is empty
public const string ALL_QUERY = "*:*";
#region index rules
// common fields used in the "all" default search field
public const string ALL_AUDIBLE_PRODUCT_ID = nameof(Book.AudibleProductId);
public const string ALL_TITLE = nameof(Book.Title);
public const string ALL_AUTHOR_NAMES = "AuthorNames";
public const string ALL_NARRATOR_NAMES = "NarratorNames";
public const string ALL_SERIES_NAMES = "SeriesNames";
#region index rules
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> idIndexRules { get; }
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> idIndexRules { get; }
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
new Dictionary<string, Func<LibraryBook, string>>
{
@@ -50,31 +50,23 @@ namespace LibationSearchEngine
[nameof(Book.DatePublished)] = lb => lb.Book.DatePublished?.ToLuceneString(),
[nameof(Book.Title)] = lb => lb.Book.Title,
[nameof(Book.AuthorNames)] = lb => lb.Book.AuthorNames,
["Author"] = lb => lb.Book.AuthorNames,
["Authors"] = lb => lb.Book.AuthorNames,
[nameof(Book.NarratorNames)] = lb => lb.Book.NarratorNames,
["Narrator"] = lb => lb.Book.NarratorNames,
["Narrators"] = lb => lb.Book.NarratorNames,
[ALL_AUTHOR_NAMES] = lb => lb.Book.AuthorNames(),
["Author"] = lb => lb.Book.AuthorNames(),
["Authors"] = lb => lb.Book.AuthorNames(),
[ALL_NARRATOR_NAMES] = lb => lb.Book.NarratorNames(),
["Narrator"] = lb => lb.Book.NarratorNames(),
["Narrators"] = lb => lb.Book.NarratorNames(),
[nameof(Book.Publisher)] = lb => lb.Book.Publisher,
[nameof(Book.SeriesNames)] = lb => string.Join(
", ",
lb.Book.SeriesLink
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.AudibleSeriesId)),
["Series"] = lb => string.Join(
", ",
lb.Book.SeriesLink
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.AudibleSeriesId)),
[ALL_SERIES_NAMES] = lb => lb.Book.SeriesNames(),
["Series"] = lb => lb.Book.SeriesNames(),
["SeriesId"] = lb => string.Join(", ", lb.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)),
[nameof(Book.CategoriesNames)] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
[nameof(Book.Category)] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
["Categories"] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
["CategoriesId"] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
["CategoryId"] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
["CategoriesNames"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()),
[nameof(Book.Category)] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()),
["Categories"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()),
["CategoriesId"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()),
["CategoryId"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()),
[TAGS.FirstCharToUpper()] = lb => lb.Book.UserDefinedItem.Tags,
@@ -107,14 +99,14 @@ namespace LibationSearchEngine
= new ReadOnlyDictionary<string, Func<LibraryBook, bool>>(
new Dictionary<string, Func<LibraryBook, bool>>
{
["HasDownloads"] = lb => lb.Book.HasPdf,
["HasDownload"] = lb => lb.Book.HasPdf,
["Downloads"] = lb => lb.Book.HasPdf,
["Download"] = lb => lb.Book.HasPdf,
["HasPDFs"] = lb => lb.Book.HasPdf,
["HasPDF"] = lb => lb.Book.HasPdf,
["PDFs"] = lb => lb.Book.HasPdf,
["PDF"] = lb => lb.Book.HasPdf,
["HasDownloads"] = lb => lb.Book.HasPdf(),
["HasDownload"] = lb => lb.Book.HasPdf(),
["Downloads"] = lb => lb.Book.HasPdf(),
["Download"] = lb => lb.Book.HasPdf(),
["HasPDFs"] = lb => lb.Book.HasPdf(),
["HasPDF"] = lb => lb.Book.HasPdf(),
["PDFs"] = lb => lb.Book.HasPdf(),
["PDF"] = lb => lb.Book.HasPdf(),
["IsRated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
["Rated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
@@ -125,15 +117,16 @@ namespace LibationSearchEngine
[nameof(Book.IsAbridged)] = lb => lb.Book.IsAbridged,
["Abridged"] = lb => lb.Book.IsAbridged,
// this will only be evaluated at time of re-index. ie: state of files moved later will be out of sync until next re-index
["IsLiberated"] = lb => isLiberated(lb.Book),
["Liberated"] = lb => isLiberated(lb.Book),
["LiberatedError"] = lb => liberatedError(lb.Book),
["Podcast"] = lb => lb.Book.ContentType == ContentType.Episode,
["IsPodcast"] = lb => lb.Book.ContentType == ContentType.Episode,
["Episode"] = lb => lb.Book.ContentType == ContentType.Episode,
["IsEpisode"] = lb => lb.Book.ContentType == ContentType.Episode,
["Podcast"] = lb => lb.Book.IsEpisodeChild(),
["Podcasts"] = lb => lb.Book.IsEpisodeChild(),
["IsPodcast"] = lb => lb.Book.IsEpisodeChild(),
["Episode"] = lb => lb.Book.IsEpisodeChild(),
["Episodes"] = lb => lb.Book.IsEpisodeChild(),
["IsEpisode"] = lb => lb.Book.IsEpisodeChild(),
}
);
@@ -151,10 +144,11 @@ namespace LibationSearchEngine
private static IEnumerable<Func<LibraryBook, string>> allFieldIndexRules { get; }
= new List<Func<LibraryBook, string>>
{
idIndexRules[nameof(Book.AudibleProductId)],
stringIndexRules[nameof(Book.Title)],
stringIndexRules[nameof(Book.AuthorNames)],
stringIndexRules[nameof(Book.NarratorNames)]
idIndexRules[ALL_AUDIBLE_PRODUCT_ID],
stringIndexRules[ALL_TITLE],
stringIndexRules[ALL_AUTHOR_NAMES],
stringIndexRules[ALL_NARRATOR_NAMES],
stringIndexRules[ALL_SERIES_NAMES]
};
#endregion
@@ -182,18 +176,6 @@ namespace LibationSearchEngine
foreach (var key in numberIndexRules.Keys)
yield return key;
}
public static IEnumerable<string> GetSearchFields()
{
foreach (var key in idIndexRules.Keys)
yield return key;
foreach (var key in stringIndexRules.Keys)
yield return key;
foreach (var key in boolIndexRules.Keys)
yield return key;
foreach (var key in numberIndexRules.Keys)
yield return key;
}
#endregion
#region create and update index
@@ -290,6 +272,10 @@ namespace LibationSearchEngine
book.AudibleProductId,
d =>
{
//
// TODO: better synonym handling. This is too easy to mess up
//
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
// ie: must remove old before adding new else will create unwanted duplicates.
var v1 = isLiberated(book);
@@ -331,6 +317,9 @@ namespace LibationSearchEngine
}
#endregion
// the workaround which allows displaying all books when query is empty
public const string ALL_QUERY = "*:*";
#region search
public SearchResultSet Search(string searchString)
{
@@ -345,7 +334,7 @@ namespace LibationSearchEngine
return results;
}
public static string FormatSearchQuery(string searchString)
internal static string FormatSearchQuery(string searchString)
{
if (string.IsNullOrWhiteSpace(searchString))
return ALL_QUERY;
@@ -491,5 +480,9 @@ namespace LibationSearchEngine
#endregion
private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
// not customizable. don't move to config
private static string SearchEngineDirectory { get; }
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("SearchEngine").FullName;
}
}

View File

@@ -1,34 +0,0 @@
using System;
using DataLayer;
using LibationFileManager;
namespace LibationWinForms.BookLiberation
{
class AudioConvertForm : AudioDecodeForm
{
public AudioConvertForm()
{
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
}
#region AudioDecodeForm overrides
public override string DecodeActionName => "Converting";
#endregion
#region Processable event handler overrides
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
LogMe.Info($"Convert Step, Begin: {libraryBook.Book}");
base.Processable_Begin(sender, libraryBook);
}
public override void Processable_Completed(object sender, LibraryBook libraryBook)
{
base.Processable_Completed(sender, libraryBook);
LogMe.Info($"Convert Step, Completed: {libraryBook.Book}{Environment.NewLine}");
}
#endregion
}
}

View File

@@ -1,104 +0,0 @@
namespace LibationWinForms.BookLiberation
{
partial class AudioDecodeForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.bookInfoLbl = new System.Windows.Forms.Label();
this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.remainingTimeLbl = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.SuspendLayout();
//
// pictureBox1
//
this.pictureBox1.Location = new System.Drawing.Point(14, 14);
this.pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(117, 115);
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false;
//
// bookInfoLbl
//
this.bookInfoLbl.AutoSize = true;
this.bookInfoLbl.Location = new System.Drawing.Point(138, 14);
this.bookInfoLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.bookInfoLbl.Name = "bookInfoLbl";
this.bookInfoLbl.Size = new System.Drawing.Size(121, 15);
this.bookInfoLbl.TabIndex = 0;
this.bookInfoLbl.Text = "[multi-line book info]";
//
// progressBar1
//
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressBar1.Location = new System.Drawing.Point(14, 143);
this.progressBar1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(611, 27);
this.progressBar1.TabIndex = 2;
//
// remainingTimeLbl
//
this.remainingTimeLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.remainingTimeLbl.Location = new System.Drawing.Point(632, 143);
this.remainingTimeLbl.Name = "remainingTimeLbl";
this.remainingTimeLbl.Size = new System.Drawing.Size(60, 31);
this.remainingTimeLbl.TabIndex = 3;
this.remainingTimeLbl.Text = "ETA:\r\n";
this.remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// DecryptForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(707, 183);
this.Controls.Add(this.remainingTimeLbl);
this.Controls.Add(this.progressBar1);
this.Controls.Add(this.bookInfoLbl);
this.Controls.Add(this.pictureBox1);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "DecryptForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "DecryptForm";
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Label bookInfoLbl;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label remainingTimeLbl;
}
}

View File

@@ -1,114 +0,0 @@
using System;
using DataLayer;
using Dinah.Core.Net.Http;
using Dinah.Core.Threading;
using LibationFileManager;
using LibationWinForms.BookLiberation.BaseForms;
namespace LibationWinForms.BookLiberation
{
public partial class AudioDecodeForm : LiberationBaseForm
{
public virtual string DecodeActionName { get; } = "Decoding";
public AudioDecodeForm() => InitializeComponent();
private Func<byte[]> GetCoverArtDelegate;
#region Processable event handler overrides
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
base.Processable_Begin(sender, libraryBook);
GetCoverArtDelegate = () => PictureStorage.GetPictureSynchronously(
new PictureDefinition(
libraryBook.Book.PictureId,
PictureSize._500x500));
//Set default values from library
AudioDecodable_TitleDiscovered(sender, libraryBook.Book.Title);
AudioDecodable_AuthorsDiscovered(sender, libraryBook.Book.AuthorNames);
AudioDecodable_NarratorsDiscovered(sender, libraryBook.Book.NarratorNames);
AudioDecodable_CoverImageDiscovered(sender,
PictureStorage.GetPicture(
new PictureDefinition(
libraryBook.Book.PictureId,
PictureSize._80x80)).bytes);
}
#endregion
#region Streamable event handler overrides
public override void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress)
{
base.Streamable_StreamingProgressChanged(sender, downloadProgress);
if (!downloadProgress.ProgressPercentage.HasValue)
return;
if (downloadProgress.ProgressPercentage == 0)
updateRemainingTime(0);
else
progressBar1.UIThreadAsync(() => progressBar1.Value = (int)downloadProgress.ProgressPercentage);
}
public override void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining)
{
base.Streamable_StreamingTimeRemaining(sender, timeRemaining);
updateRemainingTime((int)timeRemaining.TotalSeconds);
}
private void updateRemainingTime(int remaining)
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{formatTime(remaining)}");
private string formatTime(int seconds)
{
var timeSpan = new TimeSpan(0, 0, seconds);
return
timeSpan.TotalHours >= 1 ? $"{timeSpan:%h}h {timeSpan:mm}m {timeSpan:ss}s"
: timeSpan.TotalMinutes >= 1 ? $"{timeSpan:%m}m {timeSpan:ss}s"
: $"{seconds} sec";
}
#endregion
#region AudioDecodable event handlers
private string title;
private string authorNames;
private string narratorNames;
public override void AudioDecodable_TitleDiscovered(object sender, string title)
{
base.AudioDecodable_TitleDiscovered(sender, title);
this.UIThreadAsync(() => this.Text = DecodeActionName + " " + title);
this.title = title;
updateBookInfo();
}
public override void AudioDecodable_AuthorsDiscovered(object sender, string authors)
{
base.AudioDecodable_AuthorsDiscovered(sender, authors);
authorNames = authors;
updateBookInfo();
}
public override void AudioDecodable_NarratorsDiscovered(object sender, string narrators)
{
base.AudioDecodable_NarratorsDiscovered(sender, narrators);
narratorNames = narrators;
updateBookInfo();
}
private void updateBookInfo()
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
public override void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
{
base.AudioDecodable_RequestCoverArt(sender, setCoverArtDelegate);
setCoverArtDelegate(GetCoverArtDelegate?.Invoke());
}
public override void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
{
base.AudioDecodable_CoverImageDiscovered(sender, coverArt);
pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
}
#endregion
}
}

View File

@@ -1,34 +0,0 @@
using System;
using DataLayer;
using LibationFileManager;
namespace LibationWinForms.BookLiberation
{
class AudioDecryptForm : AudioDecodeForm
{
public AudioDecryptForm()
{
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
}
#region AudioDecodeForm overrides
public override string DecodeActionName => "Decrypting";
#endregion
#region Processable event handler overrides
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
LogMe.Info($"Download & Decrypt Step, Begin: {libraryBook.Book}");
base.Processable_Begin(sender, libraryBook);
}
public override void Processable_Completed(object sender, LibraryBook libraryBook)
{
base.Processable_Completed(sender, libraryBook);
LogMe.Info($"Download & Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
}
#endregion
}
}

View File

@@ -1,93 +0,0 @@
namespace LibationWinForms.BookLiberation
{
partial class AutomatedBackupsForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.keepGoingCb = new System.Windows.Forms.CheckBox();
this.logTb = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// keepGoingCb
//
this.keepGoingCb.AutoSize = true;
this.keepGoingCb.Checked = true;
this.keepGoingCb.CheckState = System.Windows.Forms.CheckState.Checked;
this.keepGoingCb.Location = new System.Drawing.Point(12, 12);
this.keepGoingCb.Name = "keepGoingCb";
this.keepGoingCb.Size = new System.Drawing.Size(325, 17);
this.keepGoingCb.TabIndex = 0;
this.keepGoingCb.Text = "Keep going. Uncheck to stop when current backup is complete";
this.keepGoingCb.UseVisualStyleBackColor = true;
//
// logTb
//
this.logTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.logTb.Location = new System.Drawing.Point(12, 48);
this.logTb.Multiline = true;
this.logTb.Name = "logTb";
this.logTb.ReadOnly = true;
this.logTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.logTb.Size = new System.Drawing.Size(960, 202);
this.logTb.TabIndex = 1;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(9, 32);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(501, 13);
this.label1.TabIndex = 2;
this.label1.Text = "NOTE: if the working directories are inside of Dropbox, some book liberation acti" +
"ons may hang indefinitely";
//
// AutomatedBackupsForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(984, 262);
this.Controls.Add(this.label1);
this.Controls.Add(this.logTb);
this.Controls.Add(this.keepGoingCb);
this.Name = "AutomatedBackupsForm";
this.Text = "Automated Backups";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.AutomatedBackupsForm_FormClosing);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.CheckBox keepGoingCb;
private System.Windows.Forms.TextBox logTb;
private System.Windows.Forms.Label label1;
}
}

View File

@@ -1,34 +0,0 @@
using System;
using System.Windows.Forms;
using Dinah.Core.Threading;
namespace LibationWinForms.BookLiberation
{
public partial class AutomatedBackupsForm : Form
{
public bool KeepGoingChecked => keepGoingCb.Checked;
public bool KeepGoing => keepGoingCb.Enabled && keepGoingCb.Checked;
public AutomatedBackupsForm()
{
InitializeComponent();
}
public void WriteLine(string text)
{
if (!IsDisposed)
logTb.UIThreadAsync(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
}
public void FinalizeUI()
{
keepGoingCb.Enabled = false;
if (!IsDisposed)
logTb.AppendText("");
}
private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false;
}
}

View File

@@ -1,163 +0,0 @@
using System;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core.Net.Http;
using Dinah.Core.Threading;
using FileLiberator;
namespace LibationWinForms.BookLiberation.BaseForms
{
public class LiberationBaseForm : Form
{
protected Streamable Streamable { get; private set; }
protected LogMe LogMe { get; private set; }
private SynchronizeInvoker Invoker { get; init; }
public LiberationBaseForm()
{
//SynchronizationContext.Current will be null until the process contains a Form.
//If this is the first form created, it will not exist until after execution
//reaches inside the constructor (after base class has been initialized).
Invoker = new SynchronizeInvoker();
}
public void RegisterFileLiberator(Streamable streamable, LogMe logMe = null)
{
if (streamable is null) return;
Streamable = streamable;
LogMe = logMe;
Subscribe(streamable);
if (Streamable is Processable processable)
Subscribe(processable);
if (Streamable is AudioDecodable audioDecodable)
Subscribe(audioDecodable);
}
#region Event Subscribers and Unsubscribers
private void Subscribe(Streamable streamable)
{
UnsubscribeStreamable(this, EventArgs.Empty);
streamable.StreamingBegin += OnStreamingBeginShow;
streamable.StreamingBegin += Streamable_StreamingBegin;
streamable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
streamable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
streamable.StreamingCompleted += Streamable_StreamingCompleted;
streamable.StreamingCompleted += OnStreamingCompletedClose;
Disposed += UnsubscribeStreamable;
}
private void Subscribe(Processable processable)
{
UnsubscribeProcessable(this, null);
processable.Begin += Processable_Begin;
processable.StatusUpdate += Processable_StatusUpdate;
processable.Completed += Processable_Completed;
//The form is created on Processable.Begin and we
//dispose of it on Processable.Completed
processable.Completed += OnCompletedDispose;
//Don't unsubscribe from Dispose because it fires when
//Streamable.StreamingCompleted closes the form, and
//the Processable events need to live past that event.
processable.Completed += UnsubscribeProcessable;
}
private void Subscribe(AudioDecodable audioDecodable)
{
UnsubscribeAudioDecodable(this, EventArgs.Empty);
audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
Disposed += UnsubscribeAudioDecodable;
}
private void UnsubscribeStreamable(object sender, EventArgs e)
{
Disposed -= UnsubscribeStreamable;
Streamable.StreamingBegin -= OnStreamingBeginShow;
Streamable.StreamingBegin -= Streamable_StreamingBegin;
Streamable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
Streamable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
Streamable.StreamingCompleted -= Streamable_StreamingCompleted;
Streamable.StreamingCompleted -= OnStreamingCompletedClose;
}
private void UnsubscribeProcessable(object sender, LibraryBook e)
{
if (Streamable is not Processable processable)
return;
processable.Completed -= UnsubscribeProcessable;
processable.Completed -= OnCompletedDispose;
processable.Completed -= Processable_Completed;
processable.StatusUpdate -= Processable_StatusUpdate;
processable.Begin -= Processable_Begin;
}
private void UnsubscribeAudioDecodable(object sender, EventArgs e)
{
if (Streamable is not AudioDecodable audioDecodable)
return;
Disposed -= UnsubscribeAudioDecodable;
audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
audioDecodable.Cancel();
}
#endregion
#region Form creation and disposal handling
/// <summary>
/// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose
/// </summary>
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThreadAsync(Close);
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThreadAsync(Dispose);
/// <summary>
/// If StreamingBegin is fired from a worker thread, the window will be created on that
/// worker thread. We need to make certain that we show the window on the UI thread (same
/// thread that created form), otherwise the renderer will be on a worker thread which
/// could cause it to freeze. Form.BeginInvoke won't work until the form is created
/// (ie. shown) because Control doesn't get a window handle until it is Shown.
/// </summary>
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.UIThreadAsync(Show);
#endregion
#region Streamable event handlers
public virtual void Streamable_StreamingBegin(object sender, string beginString) { }
public virtual void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress) { }
public virtual void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining) { }
public virtual void Streamable_StreamingCompleted(object sender, string completedString) { }
#endregion
#region Processable event handlers
public virtual void Processable_Begin(object sender, LibraryBook libraryBook) { }
public virtual void Processable_StatusUpdate(object sender, string statusUpdate) { }
public virtual void Processable_Completed(object sender, LibraryBook libraryBook) { }
#endregion
#region AudioDecodable event handlers
public virtual void AudioDecodable_TitleDiscovered(object sender, string title) { }
public virtual void AudioDecodable_AuthorsDiscovered(object sender, string authors) { }
public virtual void AudioDecodable_NarratorsDiscovered(object sender, string narrators) { }
public virtual void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt) { }
public virtual void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate) { }
#endregion
}
}

View File

@@ -1,109 +0,0 @@
using DataLayer;
using System;
namespace LibationWinForms.BookLiberation
{
partial class DownloadForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.filenameLbl = new System.Windows.Forms.Label();
this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.progressLbl = new System.Windows.Forms.Label();
this.lastUpdateLbl = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// filenameLbl
//
this.filenameLbl.AutoSize = true;
this.filenameLbl.Location = new System.Drawing.Point(12, 9);
this.filenameLbl.Name = "filenameLbl";
this.filenameLbl.Size = new System.Drawing.Size(52, 13);
this.filenameLbl.TabIndex = 0;
this.filenameLbl.Text = "[filename]";
//
// progressBar1
//
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressBar1.Location = new System.Drawing.Point(15, 67);
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(877, 23);
this.progressBar1.TabIndex = 4;
//
// progressLbl
//
this.progressLbl.Location = new System.Drawing.Point(12, 36);
this.progressLbl.Name = "progressLbl";
this.progressLbl.Size = new System.Drawing.Size(173, 13);
this.progressLbl.TabIndex = 5;
this.progressLbl.Text = "[2,999,999,999] of [2,999,999,999]";
this.progressLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// lastUpdateLbl
//
this.lastUpdateLbl.AutoSize = true;
this.lastUpdateLbl.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.lastUpdateLbl.ForeColor = System.Drawing.Color.DarkRed;
this.lastUpdateLbl.Location = new System.Drawing.Point(361, 36);
this.lastUpdateLbl.Name = "lastUpdateLbl";
this.lastUpdateLbl.Size = new System.Drawing.Size(81, 13);
this.lastUpdateLbl.TabIndex = 6;
this.lastUpdateLbl.Text = "Last updated";
this.lastUpdateLbl.Visible = false;
//
// DownloadForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(904, 102);
this.Controls.Add(this.lastUpdateLbl);
this.Controls.Add(this.progressLbl);
this.Controls.Add(this.progressBar1);
this.Controls.Add(this.filenameLbl);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "DownloadForm";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Downloading";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DownloadForm_FormClosing);
this.Load += new System.EventHandler(this.DownloadForm_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label filenameLbl;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label progressLbl;
private System.Windows.Forms.Label lastUpdateLbl;
}
}

View File

@@ -1,67 +0,0 @@
using System;
using System.Windows.Forms;
using Dinah.Core.Net.Http;
using Dinah.Core.Threading;
using LibationWinForms.BookLiberation.BaseForms;
namespace LibationWinForms.BookLiberation
{
public partial class DownloadForm : LiberationBaseForm
{
public DownloadForm()
{
InitializeComponent();
progressLbl.Text = "";
filenameLbl.Text = "";
}
#region Streamable event handler overrides
public override void Streamable_StreamingBegin(object sender, string beginString)
{
base.Streamable_StreamingBegin(sender, beginString);
filenameLbl.UIThreadAsync(() => filenameLbl.Text = beginString);
}
public override void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress)
{
base.Streamable_StreamingProgressChanged(sender, downloadProgress);
// this won't happen with download file. it will happen with download string
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
return;
progressLbl.UIThreadAsync(() => progressLbl.Text = $"{downloadProgress.BytesReceived:#,##0} of {downloadProgress.TotalBytesToReceive.Value:#,##0}");
var d = double.Parse(downloadProgress.BytesReceived.ToString()) / double.Parse(downloadProgress.TotalBytesToReceive.Value.ToString()) * 100.0;
var i = int.Parse(Math.Truncate(d).ToString());
progressBar1.UIThreadAsync(() => progressBar1.Value = i);
lastDownloadProgress = DateTime.Now;
}
#endregion
#region timer
private Timer timer { get; } = new Timer { Interval = 1000 };
private void DownloadForm_Load(object sender, EventArgs e)
{
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
private DateTime lastDownloadProgress = DateTime.Now;
private void timer_Tick(object sender, EventArgs e)
{
// if no update in the last 30 seconds, display frozen label
lastUpdateLbl.UIThreadAsync(() => lastUpdateLbl.Visible = lastDownloadProgress.AddSeconds(30) < DateTime.Now);
if (lastUpdateLbl.Visible)
{
var diff = DateTime.Now - lastDownloadProgress;
var min = (int)diff.TotalMinutes;
var minText = min > 0 ? $"{min}min " : "";
lastUpdateLbl.UIThreadAsync(() => lastUpdateLbl.Text = $"Frozen? Last download activity: {minText}{diff.Seconds}sec ago");
}
}
private void DownloadForm_FormClosing(object sender, FormClosingEventArgs e) => timer.Stop();
#endregion
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,18 +0,0 @@
using DataLayer;
namespace LibationWinForms.BookLiberation
{
internal class PdfDownloadForm : DownloadForm
{
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
base.Processable_Begin(sender, libraryBook);
LogMe.Info($"PDF Step, Begin: {libraryBook.Book}");
}
public override void Processable_Completed(object sender, LibraryBook libraryBook)
{
base.Processable_Completed(sender, libraryBook);
LogMe.Info($"PDF Step, Completed: {libraryBook.Book}");
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,336 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using LibationFileManager;
using LibationWinForms.BookLiberation.BaseForms;
namespace LibationWinForms.BookLiberation
{
// decouple serilog and form. include convenience factory method
public class LogMe
{
public event EventHandler<string> LogInfo;
public event EventHandler<string> LogErrorString;
public event EventHandler<(Exception, string)> LogError;
private LogMe()
{
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
}
public static LogMe RegisterForm(AutomatedBackupsForm form = null)
{
var logMe = new LogMe();
if (form is null)
return logMe;
logMe.LogInfo += (_, text) => form?.WriteLine(text);
logMe.LogErrorString += (_, text) => form?.WriteLine(text);
logMe.LogError += (_, tuple) =>
{
form?.WriteLine(tuple.Item2 ?? "Automated backup: error");
form?.WriteLine("ERROR: " + tuple.Item1.Message);
};
return logMe;
}
public void Info(string text) => LogInfo?.Invoke(this, text);
public void Error(string text) => LogErrorString?.Invoke(this, text);
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
}
public static class ProcessorAutomationController
{
public static async Task BackupSingleBookAsync(LibraryBook libraryBook)
{
Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId });
var logMe = LogMe.RegisterForm();
var backupBook = CreateBackupBook(logMe);
// continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
}
public static async Task BackupAllBooksAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var backupBook = CreateBackupBook(logMe);
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
}
public static async Task ConvertAllBooksAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var convertBook = CreateProcessable<ConvertToMp3, AudioConvertForm>(logMe);
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
}
public static async Task BackupAllPdfsAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
}
private static Processable CreateBackupBook(LogMe logMe)
{
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
//Chain pdf download on DownloadDecryptBook.Completed
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
{
await downloadPdf.TryProcessAsync(e);
}
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted);
return downloadDecryptBook;
}
public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false)
{
Serilog.Log.Logger.Information($"Begin {nameof(DownloadFile)} for {url}");
void onDownloadFileStreamingCompleted(object sender, string savedFile)
{
Serilog.Log.Logger.Information($"Completed {nameof(DownloadFile)} for {url}. Saved to {savedFile}");
if (showDownloadCompletedDialog)
MessageBox.Show($"File downloaded to:{Environment.NewLine}{Environment.NewLine}{savedFile}");
}
var downloadFile = new DownloadFile();
var downloadForm = new DownloadForm();
downloadForm.RegisterFileLiberator(downloadFile);
downloadFile.StreamingCompleted += onDownloadFileStreamingCompleted;
async void runDownload() => await downloadFile.PerformDownloadFileAsync(url, destination);
new Task(runDownload).Start();
}
/// <summary>
/// Create a new <see cref="Processable"/> and links it to a new <see cref="LiberationBaseForm"/>.
/// </summary>
/// <typeparam name="TProcessable">The <see cref="Processable"/> derived type to create.</typeparam>
/// <typeparam name="TForm">The <see cref="LiberationBaseForm"/> derived Form to create on <see cref="Processable.Begin"/>, Show on <see cref="Streamable.StreamingBegin"/>, Close on <see cref="Streamable.StreamingCompleted"/>, and Dispose on <see cref="Processable.Completed"/> </typeparam>
/// <param name="logMe">The logger</param>
/// <param name="completedAction">An additional event handler to handle <see cref="Processable.Completed"/></param>
/// <returns>A new <see cref="Processable"/> of type <typeparamref name="TProcessable"/></returns>
private static TProcessable CreateProcessable<TProcessable, TForm>(LogMe logMe, EventHandler<LibraryBook> completedAction = null)
where TForm : LiberationBaseForm, new()
where TProcessable : Processable, new()
{
var strProc = new TProcessable();
strProc.Begin += (sender, libraryBook) =>
{
var processForm = new TForm();
processForm.RegisterFileLiberator(strProc, logMe);
processForm.Processable_Begin(sender, libraryBook);
};
strProc.Completed += completedAction;
return strProc;
}
}
internal abstract class BackupRunner
{
protected LogMe LogMe { get; }
protected Processable Processable { get; }
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
protected BackupRunner(LogMe logMe, Processable processable, AutomatedBackupsForm automatedBackupsForm = null)
{
LogMe = logMe;
Processable = processable;
AutomatedBackupsForm = automatedBackupsForm;
}
protected abstract Task RunAsync();
protected abstract string SkipDialogText { get; }
protected abstract MessageBoxButtons SkipDialogButtons { get; }
protected abstract MessageBoxDefaultButton SkipDialogDefaultButton { get; }
protected abstract DialogResult SkipResult { get; }
public async Task RunBackupAsync()
{
AutomatedBackupsForm?.Show();
try
{
await RunAsync();
}
catch (Exception ex)
{
LogMe.Error(ex);
}
AutomatedBackupsForm?.FinalizeUI();
LogMe.Info("DONE");
}
protected async Task<bool> ProcessOneAsync(LibraryBook libraryBook, bool validate)
{
try
{
var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate);
if (statusHandler.IsSuccess)
return true;
foreach (var errorMessage in statusHandler.Errors)
LogMe.Error(errorMessage);
}
catch (Exception ex)
{
LogMe.Error(ex);
}
return showRetry(libraryBook);
}
private bool showRetry(LibraryBook libraryBook)
{
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
DialogResult? dialogResult = Configuration.Instance.BadBook switch
{
Configuration.BadBookAction.Abort => DialogResult.Abort,
Configuration.BadBookAction.Retry => DialogResult.Retry,
Configuration.BadBookAction.Ignore => DialogResult.Ignore,
Configuration.BadBookAction.Ask => null,
_ => null
};
string details;
try
{
static string trunc(string str)
=> string.IsNullOrWhiteSpace(str) ? "[empty]"
: (str.Length > 50) ? $"{str.Truncate(47)}..."
: str;
details =
$@" Title: {libraryBook.Book.Title}
ID: {libraryBook.Book.AudibleProductId}
Author: {trunc(libraryBook.Book.AuthorNames)}
Narr: {trunc(libraryBook.Book.NarratorNames)}";
}
catch
{
details = "[Error retrieving details]";
}
// if null then ask user
dialogResult ??= MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
if (dialogResult == DialogResult.Abort)
return false;
if (dialogResult == SkipResult)
{
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
}
return true;
}
}
internal class BackupSingle : BackupRunner
{
private LibraryBook _libraryBook { get; }
protected override string SkipDialogText => @"
An error occurred while trying to process this book. Skip this book permanently?
{0}
- Click YES to skip this book permanently.
- Click NO to skip the book this time only. We'll try again later.
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button2;
protected override DialogResult SkipResult => DialogResult.Yes;
public BackupSingle(LogMe logMe, Processable processable, LibraryBook libraryBook)
: base(logMe, processable)
{
_libraryBook = libraryBook;
}
protected override async Task RunAsync()
{
if (_libraryBook is not null)
await ProcessOneAsync(_libraryBook, validate: true);
}
}
internal class BackupLoop : BackupRunner
{
protected override string SkipDialogText => @"
An error occurred while trying to process this book.
{0}
- ABORT: Stop processing books.
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
protected override DialogResult SkipResult => DialogResult.Ignore;
public BackupLoop(LogMe logMe, Processable processable, AutomatedBackupsForm automatedBackupsForm)
: base(logMe, processable, automatedBackupsForm) { }
protected override async Task RunAsync()
{
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
foreach (var libraryBook in Processable.GetValidLibraryBooks(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()))
{
var keepGoing = await ProcessOneAsync(libraryBook, validate: false);
if (!keepGoing)
return;
if (AutomatedBackupsForm.IsDisposed)
break;
if (!AutomatedBackupsForm.KeepGoing)
{
if (!AutomatedBackupsForm.KeepGoingChecked)
LogMe.Info("'Keep going' is unchecked");
return;
}
}
LogMe.Info("Done. All books have been processed");
}
}
}

View File

@@ -15,12 +15,8 @@ namespace LibationWinForms.Dialogs
private const string COL_AccountName = nameof(AccountName);
private const string COL_Locale = nameof(Locale);
private Form1 _parent { get; }
public AccountsDialog(Form1 parent)
public AccountsDialog()
{
_parent = parent;
InitializeComponent();
dataGridView1.Columns[COL_AccountName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
@@ -28,6 +24,7 @@ namespace LibationWinForms.Dialogs
populateDropDown();
populateGridValues();
this.SetLibationIcon();
}
private void populateDropDown()
@@ -127,7 +124,7 @@ namespace LibationWinForms.Dialogs
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show("Error attempting to save accounts", "Error saving accounts", ex);
MessageBoxLib.ShowAdminAlert(this, "Error attempting to save accounts", "Error saving accounts", ex);
}
}

View File

@@ -205,7 +205,6 @@
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "BookDetailsDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Book Details";

View File

@@ -27,6 +27,7 @@ namespace LibationWinForms.Dialogs
public BookDetailsDialog()
{
InitializeComponent();
this.SetLibationIcon();
}
public BookDetailsDialog(LibraryBook libraryBook) : this()
{
@@ -45,15 +46,16 @@ namespace LibationWinForms.Dialogs
var t = @$"
Title: {Book.Title}
Author(s): {Book.AuthorNames}
Narrator(s): {Book.NarratorNames}
Author(s): {Book.AuthorNames()}
Narrator(s): {Book.NarratorNames()}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Category: {string.Join(" > ", Book.CategoriesNames)}
Category: {string.Join(" > ", Book.CategoriesNames())}
Purchase Date: {_libraryBook.DateAdded.ToString("d")}
".Trim();
if (!string.IsNullOrWhiteSpace(Book.SeriesNames))
t += $"\r\nSeries: {Book.SeriesNames}";
var seriesNames = Book.SeriesNames();
if (!string.IsNullOrWhiteSpace(seriesNames))
t += $"\r\nSeries: {seriesNames}";
var bookRating = Book.Rating?.ToStarString();
if (!string.IsNullOrWhiteSpace(bookRating))

View File

@@ -15,17 +15,14 @@ namespace LibationWinForms.Dialogs
private const string COL_MoveUp = nameof(MoveUp);
private const string COL_MoveDown = nameof(MoveDown);
private Form1 _parent { get; }
public EditQuickFilters(Form1 parent)
public EditQuickFilters()
{
_parent = parent;
InitializeComponent();
dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
populateGridValues();
this.SetLibationIcon();
}
private void populateGridValues()

View File

@@ -170,8 +170,10 @@
this.Controls.Add(this.templateTb);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "EditTemplateDialog";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Template";

View File

@@ -28,7 +28,11 @@ namespace LibationWinForms.Dialogs
private Templates template { get; }
private string inputTemplateText { get; }
public EditTemplateDialog() => InitializeComponent();
public EditTemplateDialog()
{
InitializeComponent();
this.SetLibationIcon();
}
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
{
this.template = ArgumentValidator.EnsureNotNull(template, nameof(template));
@@ -42,7 +46,7 @@ namespace LibationWinForms.Dialogs
if (template is null)
{
MessageBoxAlertAdmin.Show($"Programming error. {nameof(EditTemplateDialog)} was not created correctly", "Edit template error", new NullReferenceException($"{nameof(template)} is null"));
MessageBoxLib.ShowAdminAlert(this, $"Programming error. {nameof(EditTemplateDialog)} was not created correctly", "Edit template error", new NullReferenceException($"{nameof(template)} is null"));
return;
}

View File

@@ -0,0 +1,120 @@
namespace LibationWinForms.Dialogs
{
partial class LiberatedStatusBatchDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.bookLiberatedCb = new System.Windows.Forms.ComboBox();
this.bookLiberatedLbl = new System.Windows.Forms.Label();
this.liberatedDescLbl = new System.Windows.Forms.Label();
this.cancelBtn = new System.Windows.Forms.Button();
this.saveBtn = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// bookLiberatedCb
//
this.bookLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.bookLiberatedCb.FormattingEnabled = true;
this.bookLiberatedCb.Location = new System.Drawing.Point(52, 54);
this.bookLiberatedCb.Name = "bookLiberatedCb";
this.bookLiberatedCb.Size = new System.Drawing.Size(121, 23);
this.bookLiberatedCb.TabIndex = 7;
//
// bookLiberatedLbl
//
this.bookLiberatedLbl.AutoSize = true;
this.bookLiberatedLbl.Location = new System.Drawing.Point(12, 57);
this.bookLiberatedLbl.Name = "bookLiberatedLbl";
this.bookLiberatedLbl.Size = new System.Drawing.Size(34, 15);
this.bookLiberatedLbl.TabIndex = 6;
this.bookLiberatedLbl.Text = "Book";
//
// liberatedDescLbl
//
this.liberatedDescLbl.AutoSize = true;
this.liberatedDescLbl.Location = new System.Drawing.Point(12, 9);
this.liberatedDescLbl.Name = "liberatedDescLbl";
this.liberatedDescLbl.Size = new System.Drawing.Size(312, 30);
this.liberatedDescLbl.TabIndex = 5;
this.liberatedDescLbl.Text = "To download again next time: change to Not Downloaded\r\nTo not download: change to" +
" Downloaded";
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.Location = new System.Drawing.Point(464, 79);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 9;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(346, 79);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 8;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// LiberatedStatusBatchDialog
//
this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(564, 118);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.Controls.Add(this.bookLiberatedCb);
this.Controls.Add(this.bookLiberatedLbl);
this.Controls.Add(this.liberatedDescLbl);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "LiberatedStatusBatchDialog";
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Liberated status: Whether the book has been downloaded";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ComboBox bookLiberatedCb;
private System.Windows.Forms.Label bookLiberatedLbl;
private System.Windows.Forms.Label liberatedDescLbl;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Button saveBtn;
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
namespace LibationWinForms.Dialogs
{
public partial class LiberatedStatusBatchDialog : Form
{
public LiberatedStatus BookLiberatedStatus { get; private set; }
public class liberatedComboBoxItem
{
public LiberatedStatus Status { get; set; }
public string Text { get; set; }
public override string ToString() => Text;
}
public LiberatedStatusBatchDialog()
{
InitializeComponent();
this.SetLibationIcon();
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
this.bookLiberatedCb.SelectedIndex = 0;
}
private void saveBtn_Click(object sender, EventArgs e)
{
BookLiberatedStatus = ((liberatedComboBoxItem)this.bookLiberatedCb.SelectedItem).Status;
this.DialogResult = DialogResult.OK;
}
private void cancelBtn_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@@ -38,7 +38,7 @@ namespace LibationWinForms.Dialogs
this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.purchaseDateGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.gridEntryBindingSource = new LibationWinForms.SyncBindingSource(this.components);
this.gridEntryBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
this.btnRemoveBooks = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit();
@@ -164,7 +164,7 @@ namespace LibationWinForms.Dialogs
this.Controls.Add(this.btnRemoveBooks);
this.Controls.Add(this._dataGridView);
this.Name = "RemoveBooksDialog";
this.Text = "RemoveBooksDialog";
this.Text = "Remove Books from Libation's Database";
this.Shown += new System.EventHandler(this.RemoveBooksDialog_Shown);
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
@@ -176,7 +176,7 @@ namespace LibationWinForms.Dialogs
#endregion
private System.Windows.Forms.DataGridView _dataGridView;
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
private LibationWinForms.GridView.SyncBindingSource gridEntryBindingSource;
private System.Windows.Forms.Button btnRemoveBooks;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
@@ -48,6 +47,7 @@ namespace LibationWinForms.Dialogs
gridEntryBindingSource.DataSource = _removableGridEntries;
_dataGridView.Enabled = false;
this.SetLibationIcon();
}
private void _dataGridView_BindingContextChanged(object sender, EventArgs e)
@@ -76,7 +76,8 @@ namespace LibationWinForms.Dialogs
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show(
MessageBoxLib.ShowAdminAlert(
this,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
@@ -94,34 +95,22 @@ namespace LibationWinForms.Dialogs
if (selectedBooks.Count == 0)
return;
var titles = selectedBooks.Select(rge => "- " + rge.Title).ToList();
var titlesAgg = titles.Take(5).Aggregate((a, b) => $"{a}\r\n{b}");
if (titles.Count == 6)
titlesAgg += $"\r\n\r\nand 1 other";
else if (titles.Count > 6)
titlesAgg += $"\r\n\r\nand {titles.Count - 5} others";
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = MessageBoxLib.ShowConfirmationDialog(
libraryBooks,
$"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?");
string thisThese = selectedBooks.Count > 1 ? "these" : "this";
string bookBooks = selectedBooks.Count > 1 ? "books" : "book";
if (result != DialogResult.Yes)
return;
var result = MessageBox.Show(
this,
$"Are you sure you want to remove {thisThese} {selectedBooks.Count} {bookBooks} from Libation's library?\r\n\r\n{titlesAgg}",
"Remove books from Libation?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1);
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
if (result == DialogResult.Yes)
{
var idsToRemove = selectedBooks.Select(rge => rge.AudibleProductId).ToList();
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
foreach (var rEntry in selectedBooks)
_removableGridEntries.Remove(rEntry);
foreach (var rEntry in selectedBooks)
_removableGridEntries.Remove(rEntry);
UpdateSelection();
}
UpdateSelection();
}
private void UpdateSelection()
@@ -132,10 +121,8 @@ namespace LibationWinForms.Dialogs
}
}
internal class RemovableGridEntry : GridEntry
internal class RemovableGridEntry : GridView.LibraryBookEntry
{
private static readonly IComparer BoolComparer = new ObjectComparer<bool>();
private bool _remove = false;
public RemovableGridEntry(LibraryBook libraryBook) : base(libraryBook) { }
@@ -147,11 +134,8 @@ namespace LibationWinForms.Dialogs
}
set
{
if (_remove != value)
{
_remove = value;
NotifyPropertyChanged();
}
_remove = value;
NotifyPropertyChanged();
}
}
@@ -161,12 +145,5 @@ namespace LibationWinForms.Dialogs
return Remove;
return base.GetMemberValue(memberName);
}
public override IComparer GetMemberComparer(Type memberType)
{
if (memberType == typeof(bool))
return BoolComparer;
return base.GetMemberComparer(memberType);
}
}
}

View File

@@ -10,13 +10,10 @@ namespace LibationWinForms.Dialogs
{
public List<Account> CheckedAccounts { get; } = new List<Account>();
private Form1 _parent { get; }
public ScanAccountsDialog(Form1 parent)
public ScanAccountsDialog()
{
_parent = parent;
InitializeComponent();
this.SetLibationIcon();
}
private class listItem
@@ -43,7 +40,7 @@ namespace LibationWinForms.Dialogs
private void editBtn_Click(object sender, EventArgs e)
{
if (new AccountsDialog(_parent).ShowDialog() == DialogResult.OK)
if (new AccountsDialog().ShowDialog() == DialogResult.OK)
{
// clear grid
this.accountsClb.Items.Clear();

View File

@@ -113,7 +113,6 @@
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "SearchSyntaxDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Filter options";
this.ResumeLayout(false);

View File

@@ -14,6 +14,7 @@ namespace LibationWinForms.Dialogs
label3.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields());
label4.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields());
label5.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields());
this.SetLibationIcon();
}
private void CloseBtn_Click(object sender, EventArgs e) => this.Close();

View File

@@ -17,12 +17,10 @@ namespace LibationWinForms.Dialogs
private void LameMatchSourceBRCbox_CheckedChanged(object sender, EventArgs e)
{
lameBitrateTb.Enabled = !LameMatchSourceBRCbox.Checked;
lameConstantBitrateCbox.Enabled = !LameMatchSourceBRCbox.Checked;
}
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
{
lameOptionsGb.Enabled = convertLossyRb.Checked;
lameTargetRb_CheckedChanged(sender, e);
LameMatchSourceBRCbox_CheckedChanged(sender, e);
}

View File

@@ -53,6 +53,8 @@
this.tab1ImportantSettings = new System.Windows.Forms.TabPage();
this.booksGb = new System.Windows.Forms.GroupBox();
this.tab2ImportLibrary = new System.Windows.Forms.TabPage();
this.autoDownloadEpisodesCb = new System.Windows.Forms.CheckBox();
this.autoScanCb = new System.Windows.Forms.CheckBox();
this.showImportedStatsCb = new System.Windows.Forms.CheckBox();
this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage();
this.inProgressFilesGb = new System.Windows.Forms.GroupBox();
@@ -99,8 +101,8 @@
this.lameTargetBitrateRb = new System.Windows.Forms.RadioButton();
this.stripUnabridgedCbox = new System.Windows.Forms.CheckBox();
this.retainAaxFileCbox = new System.Windows.Forms.CheckBox();
this.downloadCoverArtCbox = new System.Windows.Forms.CheckBox();
this.createCueSheetCbox = new System.Windows.Forms.CheckBox();
this.autoScanCb = new System.Windows.Forms.CheckBox();
this.badBookGb.SuspendLayout();
this.tabControl.SuspendLayout();
this.tab1ImportantSettings.SuspendLayout();
@@ -243,7 +245,7 @@
// stripAudibleBrandingCbox
//
this.stripAudibleBrandingCbox.AutoSize = true;
this.stripAudibleBrandingCbox.Location = new System.Drawing.Point(19, 143);
this.stripAudibleBrandingCbox.Location = new System.Drawing.Point(19, 168);
this.stripAudibleBrandingCbox.Name = "stripAudibleBrandingCbox";
this.stripAudibleBrandingCbox.Size = new System.Drawing.Size(143, 34);
this.stripAudibleBrandingCbox.TabIndex = 13;
@@ -253,7 +255,7 @@
// splitFilesByChapterCbox
//
this.splitFilesByChapterCbox.AutoSize = true;
this.splitFilesByChapterCbox.Location = new System.Drawing.Point(19, 93);
this.splitFilesByChapterCbox.Location = new System.Drawing.Point(19, 118);
this.splitFilesByChapterCbox.Name = "splitFilesByChapterCbox";
this.splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19);
this.splitFilesByChapterCbox.TabIndex = 13;
@@ -276,7 +278,7 @@
// convertLossyRb
//
this.convertLossyRb.AutoSize = true;
this.convertLossyRb.Location = new System.Drawing.Point(19, 207);
this.convertLossyRb.Location = new System.Drawing.Point(19, 232);
this.convertLossyRb.Name = "convertLossyRb";
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
this.convertLossyRb.TabIndex = 12;
@@ -288,7 +290,7 @@
//
this.convertLosslessRb.AutoSize = true;
this.convertLosslessRb.Checked = true;
this.convertLosslessRb.Location = new System.Drawing.Point(19, 182);
this.convertLosslessRb.Location = new System.Drawing.Point(19, 207);
this.convertLosslessRb.Name = "convertLosslessRb";
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
this.convertLosslessRb.TabIndex = 11;
@@ -389,6 +391,7 @@
//
// tab2ImportLibrary
//
this.tab2ImportLibrary.Controls.Add(this.autoDownloadEpisodesCb);
this.tab2ImportLibrary.Controls.Add(this.autoScanCb);
this.tab2ImportLibrary.Controls.Add(this.showImportedStatsCb);
this.tab2ImportLibrary.Controls.Add(this.importEpisodesCb);
@@ -401,6 +404,26 @@
this.tab2ImportLibrary.Text = "Import library";
this.tab2ImportLibrary.UseVisualStyleBackColor = true;
//
// autoDownloadEpisodesCb
//
this.autoDownloadEpisodesCb.AutoSize = true;
this.autoDownloadEpisodesCb.Location = new System.Drawing.Point(6, 106);
this.autoDownloadEpisodesCb.Name = "autoDownloadEpisodesCb";
this.autoDownloadEpisodesCb.Size = new System.Drawing.Size(190, 19);
this.autoDownloadEpisodesCb.TabIndex = 5;
this.autoDownloadEpisodesCb.Text = "[auto download episodes desc]";
this.autoDownloadEpisodesCb.UseVisualStyleBackColor = true;
//
// autoScanCb
//
this.autoScanCb.AutoSize = true;
this.autoScanCb.Location = new System.Drawing.Point(6, 6);
this.autoScanCb.Name = "autoScanCb";
this.autoScanCb.Size = new System.Drawing.Size(112, 19);
this.autoScanCb.TabIndex = 1;
this.autoScanCb.Text = "[auto scan desc]";
this.autoScanCb.UseVisualStyleBackColor = true;
//
// showImportedStatsCb
//
this.showImportedStatsCb.AutoSize = true;
@@ -556,6 +579,7 @@
this.tab4AudioFileOptions.Controls.Add(this.stripUnabridgedCbox);
this.tab4AudioFileOptions.Controls.Add(this.splitFilesByChapterCbox);
this.tab4AudioFileOptions.Controls.Add(this.retainAaxFileCbox);
this.tab4AudioFileOptions.Controls.Add(this.downloadCoverArtCbox);
this.tab4AudioFileOptions.Controls.Add(this.createCueSheetCbox);
this.tab4AudioFileOptions.Controls.Add(this.allowLibationFixupCbox);
this.tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24);
@@ -893,7 +917,7 @@
// stripUnabridgedCbox
//
this.stripUnabridgedCbox.AutoSize = true;
this.stripUnabridgedCbox.Location = new System.Drawing.Point(19, 118);
this.stripUnabridgedCbox.Location = new System.Drawing.Point(19, 143);
this.stripUnabridgedCbox.Name = "stripUnabridgedCbox";
this.stripUnabridgedCbox.Size = new System.Drawing.Size(147, 19);
this.stripUnabridgedCbox.TabIndex = 13;
@@ -903,7 +927,7 @@
// retainAaxFileCbox
//
this.retainAaxFileCbox.AutoSize = true;
this.retainAaxFileCbox.Location = new System.Drawing.Point(19, 68);
this.retainAaxFileCbox.Location = new System.Drawing.Point(19, 93);
this.retainAaxFileCbox.Name = "retainAaxFileCbox";
this.retainAaxFileCbox.Size = new System.Drawing.Size(132, 19);
this.retainAaxFileCbox.TabIndex = 10;
@@ -911,6 +935,19 @@
this.retainAaxFileCbox.UseVisualStyleBackColor = true;
this.retainAaxFileCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
//
// downloadCoverArtCbox
//
this.downloadCoverArtCbox.AutoSize = true;
this.downloadCoverArtCbox.Checked = true;
this.downloadCoverArtCbox.CheckState = System.Windows.Forms.CheckState.Checked;
this.downloadCoverArtCbox.Location = new System.Drawing.Point(19, 68);
this.downloadCoverArtCbox.Name = "downloadCoverArtCbox";
this.downloadCoverArtCbox.Size = new System.Drawing.Size(162, 19);
this.downloadCoverArtCbox.TabIndex = 10;
this.downloadCoverArtCbox.Text = "[DownloadCoverArt desc]";
this.downloadCoverArtCbox.UseVisualStyleBackColor = true;
this.downloadCoverArtCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
//
// createCueSheetCbox
//
this.createCueSheetCbox.AutoSize = true;
@@ -924,16 +961,6 @@
this.createCueSheetCbox.UseVisualStyleBackColor = true;
this.createCueSheetCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
//
// autoScanCb
//
this.autoScanCb.AutoSize = true;
this.autoScanCb.Location = new System.Drawing.Point(6, 6);
this.autoScanCb.Name = "autoScanCb";
this.autoScanCb.Size = new System.Drawing.Size(112, 19);
this.autoScanCb.TabIndex = 1;
this.autoScanCb.Text = "[auto scan desc]";
this.autoScanCb.UseVisualStyleBackColor = true;
//
// SettingsDialog
//
this.AcceptButton = this.saveBtn;
@@ -944,8 +971,10 @@
this.Controls.Add(this.tabControl);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "SettingsDialog";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Settings";
@@ -1054,5 +1083,7 @@
private System.Windows.Forms.Label label16;
private System.Windows.Forms.CheckBox createCueSheetCbox;
private System.Windows.Forms.CheckBox autoScanCb;
private System.Windows.Forms.CheckBox downloadCoverArtCbox;
private System.Windows.Forms.CheckBox autoDownloadEpisodesCb;
}
}

View File

@@ -13,7 +13,11 @@ namespace LibationWinForms.Dialogs
private Configuration config { get; } = Configuration.Instance;
private Func<string, string> desc { get; } = Configuration.GetDescription;
public SettingsDialog() => InitializeComponent();
public SettingsDialog()
{
InitializeComponent();
this.SetLibationIcon();
}
private void SettingsDialog_Load(object sender, EventArgs e)
{
@@ -31,6 +35,7 @@ namespace LibationWinForms.Dialogs
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
@@ -40,6 +45,7 @@ namespace LibationWinForms.Dialogs
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt));
booksSelectControl.SetSearchTitle("books location");
booksSelectControl.SetDirectoryItems(
@@ -69,11 +75,13 @@ namespace LibationWinForms.Dialogs
lameConstantBitrateCbox.Checked = config.LameConstantBitrate;
LameMatchSourceBRCbox.Checked = config.LameMatchSourceBR;
lameVBRQualityTb.Value = config.LameVBRQuality;
downloadCoverArtCbox.Checked = config.DownloadCoverArt;
autoScanCb.Checked = config.AutoScan;
showImportedStatsCb.Checked = config.ShowImportedStats;
importEpisodesCb.Checked = config.ImportEpisodes;
downloadEpisodesCb.Checked = config.DownloadEpisodes;
autoDownloadEpisodesCb.Checked = config.AutoDownloadEpisodes;
lameTargetRb_CheckedChanged(this, e);
LameMatchSourceBRCbox_CheckedChanged(this, e);
@@ -175,7 +183,7 @@ namespace LibationWinForms.Dialogs
// only warn if changed during this time. don't want to warn every time user happens to change settings while level is verbose
if (logLevelOld != logLevelNew)
MessageBoxVerboseLoggingWarning.ShowIfTrue();
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
}
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
@@ -192,11 +200,13 @@ namespace LibationWinForms.Dialogs
config.LameConstantBitrate = lameConstantBitrateCbox.Checked;
config.LameMatchSourceBR = LameMatchSourceBRCbox.Checked;
config.LameVBRQuality = lameVBRQualityTb.Value;
config.DownloadCoverArt = downloadCoverArtCbox.Checked;
config.AutoScan = autoScanCb.Checked;
config.ShowImportedStats = showImportedStatsCb.Checked;
config.ImportEpisodes = importEpisodesCb.Checked;
config.DownloadEpisodes = downloadEpisodesCb.Checked;
config.AutoDownloadEpisodes = autoDownloadEpisodesCb.Checked;
config.InProgress = inProgressSelectControl.SelectedDirectory;

View File

@@ -0,0 +1,112 @@
namespace LibationWinForms.Dialogs
{
partial class TagsBatchDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.tagsDescLbl = new System.Windows.Forms.Label();
this.newTagsTb = new System.Windows.Forms.TextBox();
this.cancelBtn = new System.Windows.Forms.Button();
this.saveBtn = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// tagsDescLbl
//
this.tagsDescLbl.AutoSize = true;
this.tagsDescLbl.Location = new System.Drawing.Point(13, 9);
this.tagsDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.tagsDescLbl.Name = "tagsDescLbl";
this.tagsDescLbl.Size = new System.Drawing.Size(458, 15);
this.tagsDescLbl.TabIndex = 2;
this.tagsDescLbl.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
"ores";
//
// newTagsTb
//
this.newTagsTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.newTagsTb.Location = new System.Drawing.Point(13, 30);
this.newTagsTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.newTagsTb.Name = "newTagsTb";
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.newTagsTb.Size = new System.Drawing.Size(591, 23);
this.newTagsTb.TabIndex = 3;
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.Location = new System.Drawing.Point(517, 71);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 6;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(399, 71);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 5;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// TagsBatchDialog
//
this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(617, 110);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.Controls.Add(this.tagsDescLbl);
this.Controls.Add(this.newTagsTb);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "TagsBatchDialog";
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Replace Tags";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label tagsDescLbl;
private System.Windows.Forms.TextBox newTagsTb;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Button saveBtn;
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class TagsBatchDialog : Form
{
public string NewTags { get; private set; }
public TagsBatchDialog()
{
InitializeComponent();
this.SetLibationIcon();
}
private void saveBtn_Click(object sender, EventArgs e)
{
NewTags = this.newTagsTb.Text;
this.DialogResult = DialogResult.OK;
}
private void cancelBtn_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@@ -0,0 +1,119 @@
using ApplicationServices;
using Dinah.Core;
using Dinah.Core.Threading;
namespace LibationWinForms
{
public partial class Form1
{
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
protected void Configure_BackupCounts()
{
// init formattable
beginBookBackupsToolStripMenuItem.Format(0);
beginPdfBackupsToolStripMenuItem.Format(0);
pdfsCountsLbl.Text = "| [Calculating backed up PDFs]";
Load += setBackupCounts;
LibraryCommands.LibrarySizeChanged += setBackupCounts;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += exportMenuEnable;
updateCountsBw.RunWorkerCompleted += updateBottomBookNumbers;
updateCountsBw.RunWorkerCompleted += update_BeginBookBackups_menuItem;
updateCountsBw.RunWorkerCompleted += updateBottomPdfNumbers;
updateCountsBw.RunWorkerCompleted += udpate_BeginPdfOnlyBackups_menuItem;
}
private bool runBackupCountsAgain;
private void setBackupCounts(object _, object __)
{
runBackupCountsAgain = true;
if (!updateCountsBw.IsBusy)
updateCountsBw.RunWorkerAsync();
}
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
while (runBackupCountsAgain)
{
runBackupCountsAgain = false;
e.Result = LibraryCommands.GetCounts();
}
}
private void exportMenuEnable(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
exportLibraryToolStripMenuItem.Enabled = libraryStats.HasBookResults;
}
// this cannot be cleanly be FormattableToolStripMenuItem because of the optional "Errors" text
private const string backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
private void updateBottomBookNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var formatString
= !libraryStats.HasBookResults ? "No books. Begin by importing your library"
: libraryStats.booksError > 0 ? backupsCountsLbl_Format + " Errors: {3}"
: libraryStats.HasPendingBooks ? backupsCountsLbl_Format
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
var statusStripText = string.Format(formatString,
libraryStats.booksNoProgress,
libraryStats.booksDownloadedOnly,
libraryStats.booksFullyBackedUp,
libraryStats.booksError);
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
}
// update 'begin book backups' menu item
private void update_BeginBookBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var menuItemText
= libraryStats.HasPendingBooks
? $"{libraryStats.PendingBooks} remaining"
: "All books have been liberated";
menuStrip1.UIThreadAsync(() =>
{
beginBookBackupsToolStripMenuItem.Format(menuItemText);
beginBookBackupsToolStripMenuItem.Enabled = libraryStats.HasPendingBooks;
});
}
private void updateBottomPdfNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
// don't need to assign the output of Format(). It just makes this logic cleaner
var statusStripText
= !libraryStats.HasPdfResults ? ""
: libraryStats.pdfsNotDownloaded > 0 ? pdfsCountsLbl.Format(libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
}
// update 'begin pdf only backups' menu item
private void udpate_BeginPdfOnlyBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var menuItemText
= libraryStats.pdfsNotDownloaded > 0
? $"{libraryStats.pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
menuStrip1.UIThreadAsync(() =>
{
beginPdfBackupsToolStripMenuItem.Format(menuItemText);
beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0;
});
}
}
}

View File

@@ -28,391 +28,526 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.gridPanel = new System.Windows.Forms.Panel();
this.filterHelpBtn = new System.Windows.Forms.Button();
this.filterBtn = new System.Windows.Forms.Button();
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.autoScanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeLibraryBooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.convertAllM4bToMp3ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanningToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.visibleCountLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.addFilterBtn = new System.Windows.Forms.Button();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
this.SuspendLayout();
//
// gridPanel
//
this.gridPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.filterHelpBtn = new System.Windows.Forms.Button();
this.filterBtn = new System.Windows.Forms.Button();
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.autoScanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeLibraryBooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem();
this.convertAllM4bToMp3ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateVisibleToolStripMenuItem_LiberateMenu = new LibationWinForms.FormattableToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.scanningToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.visibleBooksToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem();
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu = new LibationWinForms.FormattableToolStripMenuItem();
this.replaceTagsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.setDownloadedToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.visibleCountLbl = new LibationWinForms.FormattableToolStripStatusLabel();
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new LibationWinForms.FormattableToolStripStatusLabel();
this.addQuickFilterBtn = new System.Windows.Forms.Button();
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.panel1 = new System.Windows.Forms.Panel();
this.productsDisplay = new LibationWinForms.GridView.ProductsDisplay();
this.toggleQueueHideBtn = new System.Windows.Forms.Button();
this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
this.splitContainer1.Panel1.SuspendLayout();
this.splitContainer1.Panel2.SuspendLayout();
this.splitContainer1.SuspendLayout();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
// filterHelpBtn
//
this.filterHelpBtn.Location = new System.Drawing.Point(15, 3);
this.filterHelpBtn.Margin = new System.Windows.Forms.Padding(15, 3, 4, 3);
this.filterHelpBtn.Name = "filterHelpBtn";
this.filterHelpBtn.Size = new System.Drawing.Size(26, 27);
this.filterHelpBtn.TabIndex = 3;
this.filterHelpBtn.Text = "?";
this.filterHelpBtn.UseVisualStyleBackColor = true;
this.filterHelpBtn.Click += new System.EventHandler(this.filterHelpBtn_Click);
//
// filterBtn
//
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(916, 3);
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(88, 27);
this.filterBtn.TabIndex = 2;
this.filterBtn.Text = "Filter";
this.filterBtn.UseVisualStyleBackColor = true;
this.filterBtn.Click += new System.EventHandler(this.filterBtn_Click);
//
// filterSearchTb
//
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.gridPanel.Location = new System.Drawing.Point(14, 65);
this.gridPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.gridPanel.Name = "gridPanel";
this.gridPanel.Size = new System.Drawing.Size(979, 445);
this.gridPanel.TabIndex = 5;
//
// filterHelpBtn
//
this.filterHelpBtn.Location = new System.Drawing.Point(14, 31);
this.filterHelpBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterHelpBtn.Name = "filterHelpBtn";
this.filterHelpBtn.Size = new System.Drawing.Size(26, 27);
this.filterHelpBtn.TabIndex = 3;
this.filterHelpBtn.Text = "?";
this.filterHelpBtn.UseVisualStyleBackColor = true;
this.filterHelpBtn.Click += new System.EventHandler(this.filterHelpBtn_Click);
//
// filterBtn
//
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(905, 31);
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(88, 27);
this.filterBtn.TabIndex = 2;
this.filterBtn.Text = "Filter";
this.filterBtn.UseVisualStyleBackColor = true;
this.filterBtn.Click += new System.EventHandler(this.filterBtn_Click);
//
// filterSearchTb
//
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.filterSearchTb.Location = new System.Drawing.Point(217, 33);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(681, 23);
this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
//
// menuStrip1
//
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.filterSearchTb.Location = new System.Drawing.Point(196, 7);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(712, 23);
this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
//
// menuStrip1
//
this.menuStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem,
this.liberateToolStripMenuItem,
this.exportToolStripMenuItem,
this.quickFiltersToolStripMenuItem,
this.settingsToolStripMenuItem,
this.scanningToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
this.menuStrip1.Size = new System.Drawing.Size(1007, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.scanningToolStripMenuItem,
this.visibleBooksToolStripMenuItem,
this.settingsToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
this.menuStrip1.Size = new System.Drawing.Size(1061, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.autoScanLibraryToolStripMenuItem,
this.noAccountsYetAddAccountToolStripMenuItem,
this.scanLibraryToolStripMenuItem,
this.scanLibraryOfAllAccountsToolStripMenuItem,
this.scanLibraryOfSomeAccountsToolStripMenuItem,
this.removeLibraryBooksToolStripMenuItem});
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import";
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
//
// autoScanLibraryToolStripMenuItem
//
this.autoScanLibraryToolStripMenuItem.Name = "autoScanLibraryToolStripMenuItem";
this.autoScanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.autoScanLibraryToolStripMenuItem.Text = "A&uto Scan Library";
this.autoScanLibraryToolStripMenuItem.Click += new System.EventHandler(this.autoScanLibraryToolStripMenuItem_Click);
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
this.scanLibraryToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryToolStripMenuItem_Click);
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
this.scanLibraryOfAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfAllAccountsToolStripMenuItem_Click);
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click);
//
// removeLibraryBooksToolStripMenuItem
//
this.removeLibraryBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import";
//
// autoScanLibraryToolStripMenuItem
//
this.autoScanLibraryToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.autoScanLibraryToolStripMenuItem.Name = "autoScanLibraryToolStripMenuItem";
this.autoScanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.autoScanLibraryToolStripMenuItem.Text = "A&uto Scan Library";
this.autoScanLibraryToolStripMenuItem.Click += new System.EventHandler(this.autoScanLibraryToolStripMenuItem_Click);
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
this.scanLibraryToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryToolStripMenuItem_Click);
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
this.scanLibraryOfAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfAllAccountsToolStripMenuItem_Click);
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click);
//
// removeLibraryBooksToolStripMenuItem
//
this.removeLibraryBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.removeAllAccountsToolStripMenuItem,
this.removeSomeAccountsToolStripMenuItem});
this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem";
this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books";
this.removeLibraryBooksToolStripMenuItem.Click += new System.EventHandler(this.removeLibraryBooksToolStripMenuItem_Click);
//
// removeAllAccountsToolStripMenuItem
//
this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem";
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeAllAccountsToolStripMenuItem.Text = "All Accounts";
this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click);
//
// removeSomeAccountsToolStripMenuItem
//
this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem";
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts";
this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click);
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem";
this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books";
this.removeLibraryBooksToolStripMenuItem.Click += new System.EventHandler(this.removeLibraryBooksToolStripMenuItem_Click);
//
// removeAllAccountsToolStripMenuItem
//
this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem";
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeAllAccountsToolStripMenuItem.Text = "All Accounts";
this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click);
//
// removeSomeAccountsToolStripMenuItem
//
this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem";
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts";
this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click);
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.beginBookBackupsToolStripMenuItem,
this.beginPdfBackupsToolStripMenuItem,
this.convertAllM4bToMp3ToolStripMenuItem});
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.liberateToolStripMenuItem.Text = "&Liberate";
//
// beginBookBackupsToolStripMenuItem
//
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
//
// beginPdfBackupsToolStripMenuItem
//
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
//
// convertAllM4bToMp3ToolStripMenuItem
//
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]...";
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.convertAllM4bToMp3ToolStripMenuItem,
this.liberateVisibleToolStripMenuItem_LiberateMenu});
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.liberateToolStripMenuItem.Text = "&Liberate";
//
// beginBookBackupsToolStripMenuItem
//
this.beginBookBackupsToolStripMenuItem.FormatText = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
//
// beginPdfBackupsToolStripMenuItem
//
this.beginPdfBackupsToolStripMenuItem.FormatText = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
//
// convertAllM4bToMp3ToolStripMenuItem
//
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all &M4b to Mp3 [Long-running]...";
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
//
// liberateVisibleToolStripMenuItem_LiberateMenu
//
this.liberateVisibleToolStripMenuItem_LiberateMenu.FormatText = "Liberate &Visible Books: {0}";
this.liberateVisibleToolStripMenuItem_LiberateMenu.Name = "liberateVisibleToolStripMenuItem_LiberateMenu";
this.liberateVisibleToolStripMenuItem_LiberateMenu.Size = new System.Drawing.Size(293, 22);
this.liberateVisibleToolStripMenuItem_LiberateMenu.Text = "Liberate &Visible Books: {0}";
this.liberateVisibleToolStripMenuItem_LiberateMenu.Click += new System.EventHandler(this.liberateVisible);
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportLibraryToolStripMenuItem});
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportToolStripMenuItem.Text = "E&xport";
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(156, 22);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportToolStripMenuItem.Text = "E&xport";
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(156, 22);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.firstFilterIsDefaultToolStripMenuItem,
this.editQuickFiltersToolStripMenuItem,
this.toolStripSeparator1});
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
//
// firstFilterIsDefaultToolStripMenuItem
//
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
this.firstFilterIsDefaultToolStripMenuItem.Click += new System.EventHandler(this.FirstFilterIsDefaultToolStripMenuItem_Click);
//
// editQuickFiltersToolStripMenuItem
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.EditQuickFiltersToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(253, 6);
//
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
//
// firstFilterIsDefaultToolStripMenuItem
//
this.firstFilterIsDefaultToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
this.firstFilterIsDefaultToolStripMenuItem.Click += new System.EventHandler(this.firstFilterIsDefaultToolStripMenuItem_Click);
//
// editQuickFiltersToolStripMenuItem
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.editQuickFiltersToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(253, 6);
//
// scanningToolStripMenuItem
//
this.scanningToolStripMenuItem.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.scanningToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Image = global::LibationWinForms.Properties.Resources.import_16x16;
this.scanningToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.scanningToolStripMenuItem.Name = "scanningToolStripMenuItem";
this.scanningToolStripMenuItem.Size = new System.Drawing.Size(93, 20);
this.scanningToolStripMenuItem.Text = "Scanning...";
this.scanningToolStripMenuItem.Visible = false;
//
// visibleBooksToolStripMenuItem
//
this.visibleBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu,
this.replaceTagsToolStripMenuItem,
this.setDownloadedToolStripMenuItem,
this.removeToolStripMenuItem});
this.visibleBooksToolStripMenuItem.FormatText = "&Visible Books: {0}";
this.visibleBooksToolStripMenuItem.Name = "visibleBooksToolStripMenuItem";
this.visibleBooksToolStripMenuItem.Size = new System.Drawing.Size(108, 20);
this.visibleBooksToolStripMenuItem.Text = "&Visible Books: {0}";
//
// liberateVisibleToolStripMenuItem_VisibleBooksMenu
//
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.FormatText = "&Liberate: {0}";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Name = "liberateVisibleToolStripMenuItem_VisibleBooksMenu";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Size = new System.Drawing.Size(209, 22);
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Text = "&Liberate: {0}";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Click += new System.EventHandler(this.liberateVisible);
//
// replaceTagsToolStripMenuItem
//
this.replaceTagsToolStripMenuItem.Name = "replaceTagsToolStripMenuItem";
this.replaceTagsToolStripMenuItem.Size = new System.Drawing.Size(209, 22);
this.replaceTagsToolStripMenuItem.Text = "Replace &Tags...";
this.replaceTagsToolStripMenuItem.Click += new System.EventHandler(this.replaceTagsToolStripMenuItem_Click);
//
// setDownloadedToolStripMenuItem
//
this.setDownloadedToolStripMenuItem.Name = "setDownloadedToolStripMenuItem";
this.setDownloadedToolStripMenuItem.Size = new System.Drawing.Size(209, 22);
this.setDownloadedToolStripMenuItem.Text = "Set \'&Downloaded\' status...";
this.setDownloadedToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedToolStripMenuItem_Click);
//
// removeToolStripMenuItem
//
this.removeToolStripMenuItem.Name = "removeToolStripMenuItem";
this.removeToolStripMenuItem.Size = new System.Drawing.Size(209, 22);
this.removeToolStripMenuItem.Text = "&Remove from library...";
this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click);
//
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.accountsToolStripMenuItem,
this.basicSettingsToolStripMenuItem,
this.toolStripSeparator2,
this.aboutToolStripMenuItem});
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.accountsToolStripMenuItem.Text = "&Accounts...";
this.accountsToolStripMenuItem.Click += new System.EventHandler(this.accountsToolStripMenuItem_Click);
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.basicSettingsToolStripMenuItem.Text = "&Settings...";
this.basicSettingsToolStripMenuItem.Click += new System.EventHandler(this.basicSettingsToolStripMenuItem_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(130, 6);
//
// aboutToolStripMenuItem
//
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.aboutToolStripMenuItem.Text = "A&bout...";
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click);
//
// scanningToolStripMenuItem
//
this.scanningToolStripMenuItem.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.scanningToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Image = global::LibationWinForms.Properties.Resources.import_16x16;
this.scanningToolStripMenuItem.Name = "scanningToolStripMenuItem";
this.scanningToolStripMenuItem.Size = new System.Drawing.Size(93, 20);
this.scanningToolStripMenuItem.Text = "Scanning...";
this.scanningToolStripMenuItem.Visible = false;
//
// statusStrip1
//
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.accountsToolStripMenuItem.Text = "&Accounts...";
this.accountsToolStripMenuItem.Click += new System.EventHandler(this.accountsToolStripMenuItem_Click);
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.basicSettingsToolStripMenuItem.Text = "&Settings...";
this.basicSettingsToolStripMenuItem.Click += new System.EventHandler(this.basicSettingsToolStripMenuItem_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(130, 6);
//
// aboutToolStripMenuItem
//
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.aboutToolStripMenuItem.Text = "A&bout...";
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click);
//
// statusStrip1
//
this.statusStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.visibleCountLbl,
this.springLbl,
this.backupsCountsLbl,
this.pdfsCountsLbl});
this.statusStrip1.Location = new System.Drawing.Point(0, 517);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
this.statusStrip1.Size = new System.Drawing.Size(1007, 22);
this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1";
//
// visibleCountLbl
//
this.visibleCountLbl.Name = "visibleCountLbl";
this.visibleCountLbl.Size = new System.Drawing.Size(53, 17);
this.visibleCountLbl.Text = "Visible: 0";
//
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(548, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
//
this.backupsCountsLbl.Name = "backupsCountsLbl";
this.backupsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.backupsCountsLbl.Text = "[Calculating backed up book quantities]";
//
// pdfsCountsLbl
//
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(171, 17);
this.pdfsCountsLbl.Text = "| [Calculating backed up PDFs]";
//
// addFilterBtn
//
this.addFilterBtn.Location = new System.Drawing.Point(47, 31);
this.addFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.addFilterBtn.Name = "addFilterBtn";
this.addFilterBtn.Size = new System.Drawing.Size(163, 27);
this.addFilterBtn.TabIndex = 4;
this.addFilterBtn.Text = "Add To Quick Filters";
this.addFilterBtn.UseVisualStyleBackColor = true;
this.addFilterBtn.Click += new System.EventHandler(this.AddFilterBtn_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1007, 539);
this.Controls.Add(this.filterBtn);
this.Controls.Add(this.addFilterBtn);
this.Controls.Add(this.filterSearchTb);
this.Controls.Add(this.filterHelpBtn);
this.Controls.Add(this.statusStrip1);
this.Controls.Add(this.gridPanel);
this.Controls.Add(this.menuStrip1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "Form1";
this.Text = "Libation: Liberate your Library";
this.Load += new System.EventHandler(this.Form1_Load);
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
this.statusStrip1.Location = new System.Drawing.Point(0, 618);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
this.statusStrip1.Size = new System.Drawing.Size(1061, 22);
this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1";
//
// visibleCountLbl
//
this.visibleCountLbl.FormatText = "Visible: {0}";
this.visibleCountLbl.Name = "visibleCountLbl";
this.visibleCountLbl.Size = new System.Drawing.Size(61, 17);
this.visibleCountLbl.Text = "Visible: {0}";
//
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(547, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
//
this.backupsCountsLbl.Name = "backupsCountsLbl";
this.backupsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.backupsCountsLbl.Text = "[Calculating backed up book quantities]";
//
// pdfsCountsLbl
//
this.pdfsCountsLbl.FormatText = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.pdfsCountsLbl.Text = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
//
// addQuickFilterBtn
//
this.addQuickFilterBtn.Location = new System.Drawing.Point(50, 3);
this.addQuickFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.addQuickFilterBtn.Name = "addQuickFilterBtn";
this.addQuickFilterBtn.Size = new System.Drawing.Size(137, 27);
this.addQuickFilterBtn.TabIndex = 4;
this.addQuickFilterBtn.Text = "Add To Quick Filters";
this.addQuickFilterBtn.UseVisualStyleBackColor = true;
this.addQuickFilterBtn.Click += new System.EventHandler(this.addQuickFilterBtn_Click);
//
// splitContainer1
//
this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer1.Location = new System.Drawing.Point(0, 0);
this.splitContainer1.Name = "splitContainer1";
//
// splitContainer1.Panel1
//
this.splitContainer1.Panel1.Controls.Add(this.panel1);
this.splitContainer1.Panel1.Controls.Add(this.menuStrip1);
this.splitContainer1.Panel1.Controls.Add(this.statusStrip1);
//
// splitContainer1.Panel2
//
this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1);
this.splitContainer1.Size = new System.Drawing.Size(1463, 640);
this.splitContainer1.SplitterDistance = 1061;
this.splitContainer1.SplitterWidth = 8;
this.splitContainer1.TabIndex = 7;
//
// panel1
//
this.panel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.panel1.Controls.Add(this.productsDisplay);
this.panel1.Controls.Add(this.toggleQueueHideBtn);
this.panel1.Controls.Add(this.addQuickFilterBtn);
this.panel1.Controls.Add(this.filterHelpBtn);
this.panel1.Controls.Add(this.filterSearchTb);
this.panel1.Controls.Add(this.filterBtn);
this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.panel1.Location = new System.Drawing.Point(0, 24);
this.panel1.Margin = new System.Windows.Forms.Padding(0);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(1061, 594);
this.panel1.TabIndex = 7;
//
// productsDisplay
//
this.productsDisplay.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.productsDisplay.AutoScroll = true;
this.productsDisplay.Location = new System.Drawing.Point(15, 36);
this.productsDisplay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.productsDisplay.Name = "productsDisplay";
this.productsDisplay.Size = new System.Drawing.Size(1031, 555);
this.productsDisplay.TabIndex = 9;
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
this.productsDisplay.LiberateClicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_LiberateClicked);
this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded);
//
// toggleQueueHideBtn
//
this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.toggleQueueHideBtn.Location = new System.Drawing.Point(1013, 3);
this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3);
this.toggleQueueHideBtn.Name = "toggleQueueHideBtn";
this.toggleQueueHideBtn.Size = new System.Drawing.Size(33, 27);
this.toggleQueueHideBtn.TabIndex = 8;
this.toggleQueueHideBtn.Text = "❱❱❱";
this.toggleQueueHideBtn.UseVisualStyleBackColor = true;
this.toggleQueueHideBtn.Click += new System.EventHandler(this.ToggleQueueHideBtn_Click);
//
// processBookQueue1
//
this.processBookQueue1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.processBookQueue1.Dock = System.Windows.Forms.DockStyle.Fill;
this.processBookQueue1.Location = new System.Drawing.Point(0, 0);
this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.processBookQueue1.Name = "processBookQueue1";
this.processBookQueue1.Size = new System.Drawing.Size(394, 640);
this.processBookQueue1.TabIndex = 0;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1463, 640);
this.Controls.Add(this.splitContainer1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "Form1";
this.Text = "Libation: Liberate your Library";
this.Load += new System.EventHandler(this.Form1_Load);
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.splitContainer1.Panel1.ResumeLayout(false);
this.splitContainer1.Panel1.PerformLayout();
this.splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
this.splitContainer1.ResumeLayout(false);
this.panel1.ResumeLayout(false);
this.panel1.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Panel gridPanel;
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem importToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1;
private System.Windows.Forms.ToolStripStatusLabel springLbl;
private System.Windows.Forms.ToolStripStatusLabel visibleCountLbl;
private LibationWinForms.FormattableToolStripStatusLabel visibleCountLbl;
private System.Windows.Forms.ToolStripMenuItem liberateToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel backupsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginBookBackupsToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel pdfsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginPdfBackupsToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem beginBookBackupsToolStripMenuItem;
private LibationWinForms.FormattableToolStripStatusLabel pdfsCountsLbl;
private LibationWinForms.FormattableToolStripMenuItem beginPdfBackupsToolStripMenuItem;
private System.Windows.Forms.TextBox filterSearchTb;
private System.Windows.Forms.Button filterBtn;
private System.Windows.Forms.Button filterHelpBtn;
@@ -420,7 +555,7 @@
private System.Windows.Forms.ToolStripMenuItem scanLibraryToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem quickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem firstFilterIsDefaultToolStripMenuItem;
private System.Windows.Forms.Button addFilterBtn;
private System.Windows.Forms.Button addQuickFilterBtn;
private System.Windows.Forms.ToolStripMenuItem editQuickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem basicSettingsToolStripMenuItem;
@@ -438,5 +573,16 @@
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanningToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem autoScanLibraryToolStripMenuItem;
}
private LibationWinForms.FormattableToolStripMenuItem visibleBooksToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_VisibleBooksMenu;
private System.Windows.Forms.ToolStripMenuItem replaceTagsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem setDownloadedToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
private System.Windows.Forms.SplitContainer splitContainer1;
private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Button toggleQueueHideBtn;
private LibationWinForms.GridView.ProductsDisplay productsDisplay;
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Windows.Forms;
using ApplicationServices;
namespace LibationWinForms
{
public partial class Form1
{
private void Configure_Export() { }
private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
var saveFileDialog = new SaveFileDialog
{
Title = "Where to export Library",
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
};
if (saveFileDialog.ShowDialog() != DialogResult.OK)
return;
// FilterIndex is 1-based, NOT 0-based
switch (saveFileDialog.FilterIndex)
{
case 1: // xlsx
default:
LibraryExporter.ToXlsx(saveFileDialog.FileName);
break;
case 2: // csv
LibraryExporter.ToCsv(saveFileDialog.FileName);
break;
case 3: // json
LibraryExporter.ToJson(saveFileDialog.FileName);
break;
}
MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
}
}
}
}

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