Compare commits

...

322 Commits

Author SHA1 Message Date
Robert McRackan
4001124cfa AudibleApi. Better logging around getting pdf url 2022-05-24 09:03:43 -04: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
Robert McRackan
6254585ae2 New feature: auto-library updating 2022-05-09 15:28:00 -04: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
Robert McRackan
4332732cc4 update dependencies 2022-05-09 13:24:23 -04:00
Robert McRackan
15b6b673d7 Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-05-09 11:21:16 -04:00
Robert McRackan
cb8b5d74d7 auto-scan initial code. Feature not yet complete 2022-05-09 11:21:09 -04:00
rmcrackan
535dc7a038 Update README.md 2022-05-09 11:15:33 -04:00
rmcrackan
c6f7b142ee Update README.md 2022-05-09 11:12:36 -04:00
rmcrackan
4853b26cd3 Update README.md 2022-05-09 11:09:48 -04:00
Robert McRackan
c99ee56f24 documentation: getting started 2022-05-09 11:08:57 -04:00
rmcrackan
f6a22db188 Update README.md 2022-05-09 11:04:29 -04:00
rmcrackan
c26b31f548 Update README.md 2022-05-09 11:03:42 -04:00
Robert McRackan
96654c599b move documentation 2022-05-09 11:02:38 -04:00
rmcrackan
57be386ac7 Update README.md 2022-05-09 11:01:34 -04:00
rmcrackan
09f782bfbd Update README.md 2022-05-09 10:58:08 -04:00
rmcrackan
cb673e34c1 Update README.md 2022-05-09 10:57:43 -04:00
rmcrackan
62af7ab70c Update Advanced.md 2022-05-09 10:57:08 -04:00
rmcrackan
71f2f656e8 Update Advanced.md 2022-05-09 10:56:40 -04:00
rmcrackan
fddf096de6 Update Advanced.md 2022-05-09 10:55:13 -04:00
Robert McRackan
9b8cf69148 documentation: advanced 2022-05-09 10:53:50 -04:00
Robert McRackan
628c80048b fix read me 2022-05-09 10:46:47 -04:00
Robert McRackan
9daa264af4 images => Documentation/images 2022-05-09 10:42:51 -04:00
Robert McRackan
8f8a8e7340 remove duplicate image files. also in LibationWinForms\Resources 2022-05-09 10:33:51 -04:00
Robert McRackan
389fbb2371 Move source code into "Source" folder 2022-05-09 10:31:45 -04:00
Robert McRackan
1ee73fa1a7 increment version 2022-05-09 07:58:01 -04:00
Robert McRackan
adbbff368f Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-05-09 07:56:25 -04:00
Robert McRackan
ee9d30bd56 Michael's "frame" fix from email 2022-05-09 07:56:16 -04:00
rmcrackan
5a822809a9 Merge pull request #239 from Mbucari/master
Fix temp file storage/deletion + a few new features:

* .tmp and .json files are again properly stored in DownloadsInProgressDirectory, and the are again used if resuming a download.
* Yellow stoplight works again.
* All audio files stored in DecryptInProgressDirectory are deleted if the conversion fails Partial / Complete files in %temp%\Libation are not tidied / reused #144
* Added option to retain aax file Feature Request: Allow saving the original aax #187
* Add option to remove "(Unabridged)" from metadata tags Remove "(Unabridged)" from the title? #223
* Added options for mp3 encoding
* Add option to create cue sheet
* Prevent same book being decrypted more than once at a time
* Made a new settings tab for all audio file editing/fixing options.
2022-05-08 22:13:47 -04:00
Michael Bucari-Tovo
4e587e0429 Add try block 2022-05-08 19:58:48 -06:00
Michael Bucari-Tovo
9a619186fd If keeping aaxc, write aaxc key to file 2022-05-08 19:56:59 -06:00
Michael Bucari-Tovo
eab6f71a4c Don't delete temp aaxc file if download failed. 2022-05-08 17:07:10 -06:00
Michael Bucari-Tovo
f68bf2d6b3 Better method for downloading only 1 book at a time. 2022-05-08 16:48:58 -06:00
Michael Bucari-Tovo
2afcaebb78 Prevent same bok being decrypted more than once at a time 2022-05-08 16:31:24 -06:00
Michael Bucari-Tovo
458ea6a377 Fix tmp file extension to aaxc 2022-05-08 16:13:35 -06:00
Michael Bucari-Tovo
0e2997d309 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-08 15:46:57 -06:00
Michael Bucari-Tovo
420f4b9d5d Add optional cue sheet 2022-05-08 15:46:33 -06:00
Mbucari
bcfa97219f Merge branch 'rmcrackan:master' into master 2022-05-08 14:49:07 -06:00
Michael Bucari-Tovo
4c66010afe Add user settings for mp3 encoding. 2022-05-08 14:48:43 -06:00
Michael Bucari-Tovo
05f25a88c6 Fix temp file reuse/cleanup. Add retain aax option. 2022-05-08 11:08:23 -06:00
rmcrackan
2c6c08fbb5 Merge pull request #238 from Mbucari/master
Add option to remove Audible branding audio
2022-05-08 12:57:21 -04:00
Michael Bucari-Tovo
8af60b56b6 Refactoring for clarity. 2022-05-08 09:40:21 -06:00
Michael Bucari-Tovo
c0516772a7 Move OutputFormat to DownloadLicense 2022-05-08 09:40:08 -06:00
Michael Bucari-Tovo
77de70762c Add default for StripAudibleBrandAudio option 2022-05-08 09:39:55 -06:00
Michael Bucari-Tovo
7164100cb1 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-07 23:46:55 -06:00
Michael Bucari-Tovo
9292a62015 Fix mutually exclusive setting options 2022-05-07 23:44:28 -06:00
Mbucari
5280e68da9 Update AaxDecrypter.csproj 2022-05-07 23:23:31 -06:00
Michael Bucari-Tovo
0f6b0bf9fe Add trim audio support to multipart files. 2022-05-07 22:34:28 -06:00
Michael Bucari-Tovo
510ed95590 Remove 500ms headroom 2022-05-07 21:42:23 -06:00
Michael Bucari-Tovo
9862593f4a Add option to strip Audible brand audio 2022-05-07 21:29:29 -06:00
Michael Bucari-Tovo
d595b62f13 Update aaxclean 2022-05-06 17:16:14 -06:00
Robert McRackan
12abbb79b1 Don't allow multiple simultaneous imports 2022-05-06 16:00:37 -04:00
Robert McRackan
ecaa3b9aab Scanning no longer blocks UI 2022-05-04 17:15:28 -04:00
Robert McRackan
ded175f2d2 Extract 'sortable' formatting from GridEntry 2022-05-04 13:50:23 -04:00
Robert McRackan
128facec21 add/modify grid without reload 2022-05-04 13:37:25 -04:00
Robert McRackan
0bde86ebfd QuickFilters on save event 2022-05-03 11:29:21 -04:00
Robert McRackan
28625029cd code clean up 2022-05-02 16:13:35 -04:00
Robert McRackan
1816bd721c Added initial 'About' menu with version number 2022-05-02 14:59:43 -04:00
Robert McRackan
68ad627159 update dependencies 2022-04-30 21:03:40 -04:00
Robert McRackan
878a5dd36c Libary import got a complete overhaul. On a library of 1,200 titles: initial scan is 80-85% faster. Subsequent imports are 60-70% faster 2022-04-29 16:35:49 -04:00
Robert McRackan
7c144b8277 Bug fix #234 : chapters were no longer included in the m4b file 2022-04-27 11:31:13 -04:00
Robert McRackan
bca8c3865b Expose a way to insert ad hoc library books to grid 2022-04-26 16:37:13 -04:00
Robert McRackan
58102acd35 Trivial refactoring 2022-04-26 16:34:59 -04:00
Robert McRackan
5e577843f7 Fixing genre metatag is conditional upon AllowLibationFixup setting 2022-04-26 09:49:21 -04:00
Robert McRackan
e1d549cead update dependencies 2022-04-26 09:27:13 -04:00
Robert McRackan
323b8f2fb9 minor refactors 2022-04-26 08:18:35 -04:00
Robert McRackan
3dcbcf42ed Fix 2 for issue #202 2022-04-25 22:21:36 -04:00
Robert McRackan
825078abc6 New feature: metadata correction in converted output files 2022-04-25 13:34:22 -04:00
Robert McRackan
6be44966ad * enhancement #202 : use audible category for file's genre metatag. Thanks @MBucari ! 2022-04-25 13:23:43 -04:00
rmcrackan
66da138556 Merge pull request #233 from Mbucari/master
Update Libation to work with new AAXClean.Codecs
2022-04-25 13:16:38 -04:00
Michael Bucari-Tovo
e5dd4b856e Update Libation to work with new AAXClean.Codecs 2022-04-24 19:40:34 -06:00
Robert McRackan
5caa9c5687 Improved logging for import errors 2022-04-16 16:36:49 -04:00
Robert McRackan
c8c0ffeb0d Bug fix #231 : Some books without categories are not getting imported 2022-04-15 16:30:43 -04:00
Robert McRackan
bfceb58d6b Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-04-12 09:16:10 -04:00
Robert McRackan
2e4c4cf5f7 bug fix #228 : recover from corrupt BookTags.json 2022-04-12 09:16:02 -04:00
rmcrackan
23966c9b00 Update README.md 2022-04-11 10:06:12 -04:00
rmcrackan
ef73d2243d add login details 2022-04-11 10:03:08 -04:00
Robert McRackan
c95feebd39 Add images for 'alternate login' readme 2022-04-11 09:56:59 -04:00
Robert McRackan
d6601fed83 Bug fix #225 : SaferEnumerateFiles will skip files with UnauthorizedAccessException 2022-04-08 09:11:36 -04:00
Robert McRackan
962e379642 Added debugging around file move and file delete 2022-03-30 09:52:39 -04:00
Robert McRackan
cbc61f5a2d Files and folders cannot end with dots 2022-03-25 13:51:29 -04:00
Robert McRackan
2eaac6acc2 Bug fix #210 : if attempting to paginate more than 10,000 titles : "Implied library size is unsupported" 2022-03-17 16:14:12 -04:00
Robert McRackan
03b458765c Bug fix: getting library had errors for libraries with over 10k titles 2022-03-17 13:06:55 -04:00
Robert McRackan
c8b4bc6361 Address issue #199 : Rare users can be unable to handle library scan batch size of 250. When timeout occurs, retry with batch size of 50 2022-03-06 14:21:21 -05:00
rmcrackan
d9b5725ff1 Update README.md
CLI manual change to settings file no longer needed
2022-03-03 16:59:31 -05:00
Robert McRackan
0a0f60192b Better exception logging with Serilog.Exceptions library 2022-03-03 16:54:43 -05:00
Robert McRackan
424d939c15 Update dependencies 2022-03-03 13:02:15 -05:00
Robert McRackan
87f13ff8ed Incr. ver. 2022-02-21 10:26:58 -05:00
Robert McRackan
1e24df626a Add error recovery around FileLocations.json to handle file corruption 2022-02-21 10:24:56 -05:00
Robert McRackan
0312786721 Add description to exports #197 2022-02-14 13:44:54 -05:00
Robert McRackan
1f8a5b256e Bug fix: Defensive FirstOrDefault. #194 2022-02-03 08:53:41 -05:00
rmcrackan
426391f01c Update README.md
Forgot to remove .nfo from readme
2022-01-30 09:55:34 -05:00
Robert McRackan
c296bff47f Bug fix #181 : if audible gives a null picture id then once that image is searched for, no further cover art was downloaded 2022-01-12 22:20:58 -05:00
Robert McRackan
6b649cf4ca Rare duplicates are making their way into the db. Defensive FirstOrDefault added to address bug #184 2022-01-11 07:34:42 -05:00
Robert McRackan
5103240a76 Bugfix: Latest AudibleAPI addresses #175 . Thank you again @mkb79 2022-01-04 11:25:49 -05:00
rmcrackan
c2418b10f6 Update README.md 2022-01-04 10:53:53 -05:00
Robert McRackan
d705c23472 Bug fix #158 : troublesome check is unnecessary anyway 2021-12-07 16:06:15 -05:00
Robert McRackan
de45d008c7 Bug fix #167 : folders with leading or trailing whitespace will break file saving. Including paths created from templates 2021-12-07 09:24:36 -05:00
Robert McRackan
c267332027 update dependencies 2021-12-06 15:12:44 -05:00
Robert McRackan
4829e85faf Bug fix #163 , #171 : pre-audible uk logins were failing. Thanks @mkb79 ! 2021-12-06 13:14:06 -05:00
Robert McRackan
2acb9ca7e5 Bug fix: Spent hours hunting down why database files weren't closing correctly. New to EF Core 6 "SQLite: Connections are pooled" " This results in database files being kept open by the process even after the ADO.NET connection object is closed." wtf microsoft?! 2021-12-06 12:00:12 -05:00
Robert McRackan
b260554a2a Bug fix #173 : error when importing the same book from multiple accounts during the same import 2021-12-06 11:05:46 -05:00
Robert McRackan
41a4055cd9 init default settings 2021-12-03 14:47:21 -05:00
Robert McRackan
c6e9ba9bf9 new user: init settings 2021-12-02 16:47:35 -05:00
Robert McRackan
5059333b38 Bug fix: First click on Liberated icon shows 'File not found: <temppath>'. Second click opens correct final path. #164 2021-11-30 09:54:32 -05:00
Robert McRackan
b4015030cf tl;dr text on alt login. Issue #160 2021-11-29 11:12:26 -05:00
Robert McRackan
7f5cf8f018 New config setting: ShowImportedStats -- "Show number of newly imported titles? When unchecked, no pop-up will appear after library scan." 2021-11-29 11:06:23 -05:00
rmcrackan
2c9ccd9c78 Update README.md
TOC
2021-11-29 09:05:52 -05:00
rmcrackan
cebf218db4 Update README.md
Add 'Installation'
2021-11-29 09:03:51 -05:00
Robert McRackan
530b44a0e6 Bug fix: in paths, double slashes are not allowed *except* at beginning. eg: \\192.168.0.1 (issue #157 ) 2021-11-24 13:42:11 -05:00
Robert McRackan
b3dc5a7054 Upgrade to .net6 2021-11-24 12:59:02 -05:00
Robert McRackan
2567ccb44c Enhancement: add if-series conditional logic to custom file naming. Issue #151 2021-11-11 16:43:44 -05:00
Robert McRackan
e67eac92fd Bug fix for issue #152 : individual episodes were ignored when targeted directly (as opposed to targeting the parent series) 2021-11-09 21:27:44 -05:00
Robert McRackan
6e84fd97f1 Accounts dialog: more user-friendly validation 2021-11-08 08:55:07 -05:00
Robert McRackan
9a458bf3dc Protect against blank settings.json 2021-11-05 15:25:31 -04:00
Robert McRackan
283a46e1e2 Add debugging for issue #149 2021-11-05 13:42:47 -04:00
Robert McRackan
6ff2859c39 Update dependencies 2021-11-04 16:13:43 -04:00
Robert McRackan
e8df4952fc update dependencies 2021-11-03 16:51:40 -04:00
rmcrackan
b19e1e8a30 Update README for Custom File Naming 2021-11-02 22:20:51 -04:00
Robert McRackan
a3cf6ac40d Custom file naming: manual testing complete 2021-11-02 21:53:58 -04:00
Robert McRackan
ab450c37c4 Custom File Naming complete. Final testing remains 2021-11-02 17:05:29 -04:00
Robert McRackan
c837fefbdd template file naming: code complete. Clean up and testing remain 2021-11-02 14:26:11 -04:00
Robert McRackan
46b120ee41 Bug fix: slashes in template values (eg: title) breaks file management #145 2021-11-01 11:42:05 -04:00
Robert McRackan
cae8ca7ef3 Template error and warning checks return specific errors/warnings in addition to bools 2021-10-29 17:05:57 -04:00
Robert McRackan
904665da7f Bug fix: #143 2021-10-28 20:58:32 -04:00
Robert McRackan
2478c61df6 Bugfix: template validation was opposite #142 2021-10-28 14:38:01 -04:00
Robert McRackan
288ed75b5d increm ver 2021-10-27 20:46:02 -04:00
Robert McRackan
ad5efbd9a9 Bug fix for #141 2021-10-27 20:45:21 -04:00
Robert McRackan
7eb7b2a0f9 better formatting for download/decrypt ETA 2021-10-27 17:05:17 -04:00
Robert McRackan
d0051c0f02 Add templates to settings dialog incl load validate save. Edit buttons are in place but currently do nothing 2021-10-27 16:51:31 -04:00
Robert McRackan
d20517063e Settings: single screen => tabs 2021-10-27 15:50:41 -04:00
Robert McRackan
bcca69a102 Bug fix. Wrong template referenced 2021-10-26 16:35:08 -04:00
Robert McRackan
35f8c05106 File naming is Configuration driven: Configuration, AudioFileStorageExt, Templates, TemplateTags, 2021-10-26 16:18:27 -04:00
Robert McRackan
a3d38e082d Path.GetInvalidPathChars() acts differently in C# interactive vs live code 2021-10-26 13:54:37 -04:00
Robert McRackan
b2e956e70b Update dependencies 2021-10-26 13:06:24 -04:00
Robert McRackan
e5119357b2 File naming is fully template driven 2021-10-22 17:06:42 -04:00
Robert McRackan
b42ff827d5 GetStandardizedExtension unit tests 2021-10-22 13:09:05 -04:00
Robert McRackan
68da9779da Expose internal to Test projects 2021-10-22 11:07:18 -04:00
Robert McRackan
8e358d8f04 expose library book to multipart decrypter for file naming 2021-10-21 16:43:49 -04:00
Robert McRackan
0a986238bc Defensive FirstOrDefault 2021-10-21 15:39:53 -04:00
Robert McRackan
d636ceed8e File naming stuff is (finally) centralized under AudioFileStorageExt 2021-10-21 14:38:59 -04:00
Robert McRackan
e4fc104afe Naming logic for all new files can now originate from domian logic 2021-10-20 10:56:07 -04:00
Robert McRackan
87e3075fb3 Rename InternalUtilities to AudibleUtilities 2021-10-19 10:22:42 -04:00
390 changed files with 13815 additions and 6156 deletions

View File

@@ -1,24 +0,0 @@
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public class DownloadLicense
{
public string DownloadUrl { get; }
public string AudibleKey { get; }
public string AudibleIV { get; }
public string UserAgent { get; }
public ChapterInfo ChapterInfo { get; set; }
public DownloadLicense(string downloadUrl, string audibleKey, string audibleIV, string userAgent)
{
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check. unencrypted files do not have these
AudibleKey = audibleKey;
AudibleIV = audibleIV;
}
}
}

View File

@@ -1,446 +0,0 @@
using Dinah.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using System.Net;
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}.");
if (response.Headers.GetValues("Accept-Ranges").FirstOrDefault(r => r.EqualsInsensitive("bytes")) is null)
throw new WebException($"Server at {Uri.Host} does not support Http ranges");
//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
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => ContentLength + offset,
_ => offset,
};
WaitToPosition(newPosition);
return _readFile.Position = newPosition;
}
/// <summary>
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
{
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
}
public override void Close()
{
IsCancelled = true;
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
_readFile.Close();
_writeFile.Close();
_networkStream?.Close();
Update();
}
#endregion
~NetworkFileStream()
{
downloadEnded?.Close();
downloadedPiece?.Close();
}
}
}

View File

@@ -1,38 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace DataLayer.Utilities
{
public static class LocalDatabaseInfo
{
public static List<string> GetLocalDBInstances()
{
// Start the child process.
using var p = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
UseShellExecute = false,
RedirectStandardOutput = true,
FileName = "cmd.exe",
Arguments = "/C sqllocaldb info",
CreateNoWindow = true,
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
}
};
p.Start();
var output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
// if LocalDb is not installed then it will return that 'sqllocaldb' is not recognized as an internal or external command operable program or batch file
return string.IsNullOrWhiteSpace(output) || output.Contains("not recognized")
? new List<string>()
: output
.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
.Select(i => i.Trim())
.Where(i => !string.IsNullOrEmpty(i))
.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;"
}
}

79
Documentation/Advanced.md Normal file
View File

@@ -0,0 +1,79 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Advanced: Table of Contents
- [Files and folders](#files-and-folders)
- [Linux and Mac (unofficial)](#linux-and-mac)
- [Settings](#settings)
- [Custom File Naming](#custom-file-naming)
- [Command Line Interface](#command-line-interface)
### Files and folders
To make upgrades and reinstalls easier, Libation separates all of its responsibilities to a few different folders. If you don't want to mess with this stuff: ignore it. Read on if you like a little more control over your files.
* In Libation's initial folder are the files that make up the program. Since nothing else is here, just copy new files here to upgrade the program. Delete this folder to delete Libation.
* In a separate folder, Libation keeps track of all of the files it creates like settings and downloaded images. After an upgrade, Libation might think that's its being run for the first time. Just click ADVANCED SETUP and point to this folder. Libation will reload your library and settings.
* The last important folder is the "books location." This is where Libation looks for your downloaded and decrypted books. This is how it knows which books still need to be downloaded. The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
### Linux and Mac
Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592))
### Settings
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
### Custom File Naming
In Settings, on the Download/Decrypt tab, you can specify the format in which you want your files to be named. As you edit these templates, a live example will be shown. Parameters are listed for folders, files, and files split by chapter including an explanation of what each naming option means. For instance: you can use template `<title short> - <ch# 0> of <ch count> - <ch title>` to create the file `A Study in Scarlet - 04 of 10 - A Flight for Life.m4b`.
These templates apply to GUI and CLI.
### Command Line Interface
Libationcli.exe allows limited access to Libation's functionalities as a CLI.
Warnings about relying solely on on the CLI:
* CLI will not perform any upgrades.
* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
```
help
libationcli --help
verb-specific help
libationcli scan --help
scan all libraries
libationcli scan
scan only libraries for specific accounts
libationcli scan nickname1 nickname2
convert all m4b files to mp3
libationcli convert
liberate all books and pdfs
libationcli liberate
liberate pdfs only
libationcli liberate --pdf
libationcli liberate -p
export library to file
libationcli export --path "C:\foo\bar\my.json" --json
libationcli export -p "C:\foo\bar\my.json" -j
libationcli export -p "C:\foo\bar\my.csv" --csv
libationcli export -p "C:\foo\bar\my.csv" -c
libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
libationcli export -p "C:\foo\bar\my.xlsx" -x
```

View File

@@ -0,0 +1,138 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Getting started: Table of Contents
- [Download Libation](#download-libation-1)
- [Installation](#installation)
- [Create Accounts](#create-accounts)
- [Import your library](#import-your-library)
- [Download your books -- DRM-free!](#download-your-books----drm-free)
- [Download PDF attachments](#download-pdf-attachments)
- [Details of downloaded files](#details-of-downloaded-files)
- [Export your library](#export-your-library)
### [Download Libation](https://github.com/rmcrackan/Libation/releases)
### Installation
To install Libation, extract the zip file to a folder, for example `C:\Libation`, and then run Libation.exe from that folder to begin the configuration process and configure your account(s).
### Create Accounts
Create your account(s):
![Create your accounts, menu](images/v40_accounts.png)
New locale options include many more regions including old audible accounts which pre-date the amazon acquisition
![Choose your account locales](images/v40_locales.png)
### Import your library
Be default, Libation will periodically scan the accounts you added above with a checkbox next to them. Nothing for you to do. You can also scan manually.
Select Import > Scan Library:
![Import step 1](images/Import1.png)
Or if you have multiple accounts, you'll get to choose whether to scan all accounts or just the ones you select:
![Import which accounts](images/v40_import.png)
If this is a new installation, or you're scanning an account you haven't scanned before, you'll be prompted to enter your password for the Audible account.
![Login password](images/alt-login1.png)
Enter the password and click Submit. Audible will prompt you with a CAPTCHA image.
![Login captcha](images/alt-login2.png)
Enter the CAPTCHA answer characters and click Submit. If all has gone well, Libation will start scanning the account.
In rare instances, the Captcha image/response will fail in an endless loop. If this happens, delete the problem account, and then click Save. Re-add the account and click Save again. Now try to scan the account again. This time, instead of typing your password, click the link that says "Or click here". This will open the Audible External Login dialog shown below.
![Login alternative setup](images/alt-login3.png)
You can either copy the URL shown and paste it into your browser or launch the browser directly by clicking Launch in Browser. Audible will display its standard login page. Login, including answering the CAPTCHA on the next page. In some cases, you might have to approve the login from the email account associated with that login, but once the login is successful, you'll see an error message.
![Login alternative login result](images/alt-login4.png)
This actually means you've successfully logged in. Copy the entire URL shown in your browser and return to Libation. Paste that URL into the text box at the bottom of the Audible External Login window and click Submit.
You'll see this window while it's scanning:
![Import step 2](images/Import2.png)
Success! We see how many new titles are imported:
![Import step 3](images/Import3.png)
### Download your books -- DRM-free!
Automatically download some or all of your audible books. This shows you how much of your library is not yet downloaded and decrypted:
The stoplights will tell you a title's status:
* Green: downloaded and decrypted
* Yellow: downloaded but still encrypted with DRM
* Red: not downloaded
* PDF icon without arrow: downloaded
* PDF with arrow: not downloaded
Or hover over the button to see the status.
![Liberate book step 1](images/LiberateBook1.png)
Select Liberate > Begin Book Backups
You can also click on the stop light to download only that title and its PDF
![Liberate book step 2](images/LiberateBook2.png)
First the original book with DRM is downloaded
![Liberate book step 3](images/LiberateBook3.png)
Then it's decrypted so you can use it on any device you choose. The very first time you decrypt a book, this step will take a while. Every other book will go much faster. The first time, Libation has to figure out the special decryption key which allows your personal books to be unlocked.
![Liberate book step 4](images/LiberateBook4.png)
And voila! If you have multiple books not yet liberated, Libation will automatically move on to the next.
![Liberate book step 5](images/LiberateBook5.png)
The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
### Download PDF attachments
For books which include PDF downloads, Libation can download these for you as well and will attempt to store them with the book. "Book backup" will already download an available PDF. This additional option is useful when Audible adds a PDF to your book after you've already backed it up.
Select Liberate > Begin PDF Backups
![PDF download step 2](images/PdfDownload2.png)
The downloads work just like with books, only with no additional decryption needed.
![PDF download step 3](images/PdfDownload3.png)
### Details of downloaded files
![Post download](images/PostDownload.png)
When you set up Libation, you'll specify a Books directory. Libation looks inside that directory and all subdirectories to look for files or folders with each library book's audible id. This way, organization is completely up to you. When you download + decrypt a book, you get several files
* .m4b: your audiobook in m4b format. This is the most pure version of your audiobook and retains the highest quality. Now that it's decrypted, you can play it on any audio player and put it on any device. If you'd like, you can also use 3rd party tools to turn it into an mp3. The freedom to do what you want with your files was the original inspiration for Libation.
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
### Export your library
![Export](images/Export.png)
Export your library to Excel, CSV, or JSON

View File

@@ -0,0 +1,79 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Searching and filtering: Table of Contents
- [Tags](#tags)
- [Searches](#searches)
- [Search examples](#search-examples)
- [Filters](#filters)
### Tags
To add tags to a title, click the tags button
![Tags step 1](images/Tags1.png)
Add as many tags as you'd like. Tags are separated by a space. Each tag can contain letters, numbers, and underscores
![Tags step 2](images/Tags2.png)
Tags are saved non-case specific for easy search. There is one special tag "hidden" which will also grey-out the book
![Tags step 3](images/Tags3.png)
To edit tags, just click the button again.
### Searches
Libation's advanced searching is built on the powerful Lucene search engine. Simple searches are effortless and powerful searches are simple. To search, just type and click Filter or press enter
* Type anything in the search box to search common fields: title, authors, narrators, and the book's audible id
* Use Lucene's "Query Parser Syntax" for advanced searching.
* Easy tutorial: http://www.lucenetutorial.com/lucene-query-syntax.html
* Full official guide: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html
* Tons of search fields, specific to audiobooks
* Synonyms so you don't have to memorize magic words. Eg: author and author**s** will both work
* Click [?] button for a full list of search fields and synonyms ![Filter options](images/FilterOptionsButton.png)
* Search by tag like \[this\]
* When tags have an underscore you can use part of the tag. This is useful for quick categories. The below examples make this more clear.
### Search examples
Search for anything with the word potter
![Search example: potter](images/SearchExamplePotter.png)
If you only want to see Harry Potter
![Search example: "harry potter"](images/SearchExampleHarryPotter.png)
If you only want to see potter except for Harry Potter. You can also use "-" instead of "NOT"
![Search example: "potter NOT harry"](images/SearchExamplePotterNotHarry.png)
![Search example: "potter -harry"](images/SearchExamplePotterNotHarry2.png)
To see only books written by Neil Gaiman where he also narrates his own book. (If you don't include AND, you'll see everything written by Neil Gaiman and also all books in your library which are self-narrated.)
![Search example: author:gaiman AND authornarrated](images/SearchExampleGaimanAuthorNarrated.png)
I tagged autobiographies as auto_bio and biographies written by someone else as bio. I can get only autobiographies with \[auto_bio\] or get both by searching \[bio\]
![Search example: \[bio\]](images/SearchExampleBio.png)
![Search example: \[auto_bio\]](images/SearchExampleAutoBio.png)
### Filters
If you have a search you want to save, click Add To Quick Filters to save it in your Quick Filters list. To use it again, select it from the Quick Filters list.
To edit this list go to Quick Filters > Edit quick filters. Here you can re-order the list, delete filters, double-click a filter to edit it, or double-click the bottom blank box to add a new filter.
Check "Quick Filters > Start Libation with 1st filter Default" to have your top filter automatically applied when Libation starts. In this top example, I want to always start without these: at books I've tagged hidden, books I've tagged as free_audible_originals, and books which I have rated.
![default filters](images/FiltersDefault.png)

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 336 B

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,107 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{
public class ContributorImporter : ItemsImporterBase
{
public ContributorImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new ContributorValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// get distinct
var authors = importItems
.Select(i => i.DtoItem)
.GetAuthorsDistinct()
.ToList();
var narrators = importItems
.Select(i => i.DtoItem)
.GetNarratorsDistinct()
.ToList();
var publishers = importItems
.Select(i => i.DtoItem)
.GetPublishersDistinct()
.ToList();
// load db existing => .Local
var allNames = publishers
.Union(authors.Select(n => n.Name))
.Union(narrators.Select(n => n.Name))
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
loadLocal_contributors(allNames);
// upsert
var qtyNew = 0;
qtyNew += upsertPeople(authors);
qtyNew += upsertPeople(narrators);
qtyNew += upsertPublishers(publishers);
return qtyNew;
}
private void loadLocal_contributors(List<string> contributorNames)
{
// must include default/empty/missing
contributorNames.Add(Contributor.GetEmpty().Name);
//// BAD: very inefficient
// var x = context.Contributors.Local.Where(c => !contribNames.Contains(c.Name));
// GOOD: Except() is efficient. Due to hashing, it's close to O(n)
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var remainingContribNames = contributorNames
.Distinct()
.Except(localNames)
.ToList();
// load existing => local
if (remainingContribNames.Any())
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name)).ToList();
}
// only use after loading contributors => local
private int upsertPeople(List<Person> people)
{
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var newPeople = people
.Select(p => p.Name)
.Distinct()
.Except(localNames)
.ToList();
var groupby = people.GroupBy(
p => p.Name,
p => p,
(key, g) => new { Name = key, People = g.ToList() }
);
foreach (var name in newPeople)
{
var p = groupby.Single(g => g.Name == name).People.First();
DbContext.Contributors.Add(new Contributor(p.Name, p.Asin));
}
return newPeople.Count;
}
// only use after loading contributors => local
private int upsertPublishers(List<string> publishers)
{
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var newPublishers = publishers
.Distinct()
.Except(localNames)
.ToList();
foreach (var pub in newPublishers)
DbContext.Contributors.Add(new Contributor(pub));
return newPublishers.Count;
}
}
}

View File

@@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{
public class SeriesImporter : ItemsImporterBase
{
public SeriesImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new SeriesValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// get distinct
var series = importItems
.Select(i => i.DtoItem)
.GetSeriesDistinct()
.ToList();
// load db existing => .Local
loadLocal_series(series);
// upsert
var qtyNew = upsertSeries(series);
return qtyNew;
}
private void loadLocal_series(List<AudibleApi.Common.Series> series)
{
var seriesIds = series.Select(s => s.SeriesId).ToList();
var localIds = DbContext.Series.Local.Select(s => s.AudibleSeriesId).ToList();
var remainingSeriesIds = seriesIds
.Distinct()
.Except(localIds)
.ToList();
if (remainingSeriesIds.Any())
DbContext.Series.Where(s => remainingSeriesIds.Contains(s.AudibleSeriesId)).ToList();
}
private int upsertSeries(List<AudibleApi.Common.Series> requestedSeries)
{
var qtyNew = 0;
foreach (var s in requestedSeries)
{
var series = DbContext.Series.Local.FirstOrDefault(c => c.AudibleSeriesId == s.SeriesId);
if (series is null)
{
series = DbContext.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity;
qtyNew++;
}
series.UpdateName(s.SeriesName);
}
return qtyNew;
}
}
}

View File

@@ -1,217 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AaxDecrypter;
using AudibleApi;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using FileManager;
using LibationFileManager;
namespace FileLiberator
{
public class DownloadDecryptBook : AudioDecodable
{
private AudiobookDownloadBase abDownloader;
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
public override void Cancel() => abDownloader?.Cancel();
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
var entries = new List<FilePathCache.CacheEntry>();
// these only work so minimally b/c CacheEntry is a record.
// in case of parallel decrypts, only capture the ones for this book id.
// if user somehow starts multiple decrypts of the same book in parallel: on their own head be it
void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e)
{
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
entries.Add(e);
}
void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e)
{
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
entries.Remove(e);
}
OnBegin(libraryBook);
try
{
if (libraryBook.Book.Audio_Exists)
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
bool success = false;
try
{
FilePathCache.Inserted += FilePathCache_Inserted;
FilePathCache.Removed += FilePathCache_Removed;
success = await downloadAudiobookAsync(libraryBook);
}
finally
{
FilePathCache.Inserted -= FilePathCache_Inserted;
FilePathCache.Removed -= FilePathCache_Removed;
}
// decrypt failed
if (!success)
return new StatusHandler { "Decrypt failed" };
// moves new files from temp dir to final dest
var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, entries);
// decrypt failed
if (!movedAudioFile)
return new StatusHandler { "Cannot find final audio file after decryption" };
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
return new StatusHandler();
}
finally
{
OnCompleted(libraryBook);
}
}
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
{
OnStreamingBegin($"Begin decrypting {libraryBook}");
try
{
downloadValidation(libraryBook);
var api = await libraryBook.GetApiAsync();
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
var audiobookDlLic = new DownloadLicense
(
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
contentLic?.Voucher?.Key,
contentLic?.Voucher?.Iv,
Resources.USER_AGENT
);
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
//I also assume that if DrmType != Adrm, the file will be an mp3.
//These assumptions may be wrong, and only time and bug reports will tell.
var outputFormat =
contentLic.ContentMetadata.ContentReference.ContentFormat == "MPEG" ||
(Configuration.Instance.AllowLibationFixup && Configuration.Instance.DecryptToLossy) ?
OutputFormat.Mp3 : OutputFormat.M4b;
if (Configuration.Instance.AllowLibationFixup || outputFormat == OutputFormat.Mp3)
{
audiobookDlLic.ChapterInfo = new AAXClean.ChapterInfo();
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
}
var outFileName = FileUtility.GetValidFilename(AudibleFileStorage.DecryptInProgressDirectory, libraryBook.Book.Title, outputFormat.ToString().ToLower(), libraryBook.Book.AudibleProductId);
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
abDownloader
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm ? new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic)
: Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(outFileName, cacheDir, audiobookDlLic, outputFormat)
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic, outputFormat);
abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress);
abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining);
abDownloader.RetrievedTitle += (_, title) => OnTitleDiscovered(title);
abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors);
abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators);
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
// REAL WORK DONE HERE
var success = await Task.Run(abDownloader.Run);
return success;
}
finally
{
OnStreamingCompleted($"Completed downloading and decrypting {libraryBook.Book.Title}");
}
}
private static void downloadValidation(LibraryBook libraryBook)
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
string errorTitle()
{
var title
= (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle;
};
if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale"));
}
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{
if (e is not null)
OnCoverImageDiscovered(e);
else if (Configuration.Instance.AllowLibationFixup)
OnRequestCoverArt(abDownloader.SetCoverArt);
}
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
private static bool moveFilesToBooksDir(Book book, List<FilePathCache.CacheEntry> entries)
{
// create final directory. move each file into it
var title = book.Title;
var asin = book.AudibleProductId;
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
var underscoreIndex = title.IndexOf(':');
var titleDir
= underscoreIndex < 4
? title
: title.Substring(0, underscoreIndex);
var destinationDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, asin);
Directory.CreateDirectory(destinationDir);
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
if (getFirstAudio() == default)
return false;
for (var i = 0; i < entries.Count; i++)
{
var entry = entries[i];
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
FilePathCache.Insert(book.AudibleProductId, realDest);
// propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
entries[i] = entry with { Path = realDest };
}
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
if (cue != default)
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
AudibleFileStorage.Audio.Refresh();
return true;
}
}
}

View File

@@ -1,32 +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>();
progress.ProgressChanged += (_, e) => OnStreamingProgressChanged(e);
OnStreamingBegin(proposedDownloadFilePath);
try
{
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
OnFileCreated("Upgrade", actualDownloadedFilePath);
return actualDownloadedFilePath;
}
finally
{
OnStreamingCompleted(proposedDownloadFilePath);
}
}
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core;
namespace FileLiberator
{
public static class UtilityExtensions
{
public static (string id, string title, string locale, string account) LogFriendly(this LibraryBook libraryBook)
=> (
id: libraryBook.Book.AudibleProductId,
title: libraryBook.Book.Title,
locale: libraryBook.Book.Locale,
account: libraryBook.Account.ToMask()
);
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
{
var apiExtended = await InternalUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
return apiExtended.Api;
}
}
}

View File

@@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="2.0.1.1" />
<PackageReference Include="Polly" Version="7.2.2" />
</ItemGroup>
</Project>

View File

@@ -1,188 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using Polly;
using Polly.Retry;
namespace FileManager
{
public static class FileUtility
{
/// <summary>
/// "txt" => ".txt"
/// <br />".txt" => ".txt"
/// <br />null or whitespace => ""
/// </summary>
public static string GetStandardizedExtension(string extension)
=> string.IsNullOrWhiteSpace(extension)
? (extension ?? "")?.Trim()
: '.' + extension.Trim('.');
public static string GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(filename, nameof(filename));
var template = $"<title> [<id>]";
var fullfilename = Path.Combine(dirFullPath, template + GetStandardizedExtension(extension));
var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
fileTemplate.AddParameterReplacement("title", filename);
fileTemplate.AddParameterReplacement("id", metadataSuffix);
return fileTemplate.GetFilename();
}
public static string GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix)
{
ArgumentValidator.EnsureNotNull(originalPath, nameof(originalPath));
ArgumentValidator.EnsureGreaterThan(partsPosition, nameof(partsPosition), 0);
ArgumentValidator.EnsureGreaterThan(partsTotal, nameof(partsTotal), 0);
if (partsPosition > partsTotal)
throw new ArgumentException($"{partsPosition} may not be greater than {partsTotal}");
// 1-9 => 1-9
// 10-99 => 01-99
// 100-999 => 001-999
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
var template = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
var fileTemplate = new FileTemplate(template) { IllegalCharacterReplacements = " " };
fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileTemplate.AddParameterReplacement("title", suffix);
return fileTemplate.GetFilename();
}
private const int MAX_FILENAME_LENGTH = 255;
private const int MAX_DIRECTORY_LENGTH = 247;
/// <summary>
/// Ensure valid file name path:
/// <br/>- remove invalid chars
/// <br/>- ensure uniqueness
/// <br/>- enforce max file length
/// </summary>
public static string GetValidFilename(string path, string illegalCharacterReplacements = "")
{
ArgumentValidator.EnsureNotNull(path, nameof(path));
// remove invalid chars
path = GetSafePath(path, illegalCharacterReplacements);
// ensure uniqueness and check lengths
var dir = Path.GetDirectoryName(path);
dir = dir.Truncate(MAX_DIRECTORY_LENGTH);
var filename = Path.GetFileNameWithoutExtension(path);
var fileStem = Path.Combine(dir, filename);
var extension = Path.GetExtension(path);
var fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - extension.Length) + extension;
var i = 0;
while (File.Exists(fullfilename))
{
var increm = $" ({++i})";
fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - increm.Length - extension.Length) + increm + extension;
}
return fullfilename;
}
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
/// <summary>Use with file name, not full path. Valid path charaters which are invalid file name characters will be replaced: ':', '\\', '/'</summary>
public static string GetSafeFileName(string str, string illegalCharacterReplacements = "")
=> string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
/// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary>
public static string GetSafePath(string path, string illegalCharacterReplacements = "")
{
ArgumentValidator.EnsureNotNull(path, nameof(path));
var fixedPath = string
.Join(illegalCharacterReplacements ?? "", path.Split(Path.GetInvalidPathChars()))
.Replace("*", illegalCharacterReplacements)
.Replace("?", illegalCharacterReplacements)
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
// replace all colons except within the first 2 chars
var builder = new System.Text.StringBuilder();
for (var i = 0; i < fixedPath.Length; i++)
{
var c = fixedPath[i];
if (i >= 2 && c == ':')
builder.Append(illegalCharacterReplacements);
else
builder.Append(c);
}
fixedPath = builder.ToString();
return fixedPath;
}
/// <summary>
/// Move file.
/// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length
/// <br/>- Perform <see cref="SaferMove"/>
/// <br/>- Return valid path
/// </summary>
public static string SaferMoveToValidPath(string source, string destination)
{
destination = GetValidFilename(destination);
SaferMove(source, destination);
return destination;
}
private static int maxRetryAttempts { get; } = 3;
private static TimeSpan pauseBetweenFailures { get; } = TimeSpan.FromMilliseconds(100);
private static RetryPolicy retryPolicy { get; } =
Policy
.Handle<Exception>()
.WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailures);
/// <summary>Delete file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
public static void SaferDelete(string source)
=> retryPolicy.Execute(() =>
{
try
{
if (File.Exists(source))
{
File.Delete(source);
Serilog.Log.Logger.Information("File successfully deleted", new { source });
}
}
catch (Exception e)
{
Serilog.Log.Logger.Error(e, "Failed to delete file", new { source });
throw;
}
});
/// <summary>Move file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
public static void SaferMove(string source, string destination)
=> retryPolicy.Execute(() =>
{
try
{
if (File.Exists(source))
{
SaferDelete(destination);
Directory.CreateDirectory(Path.GetDirectoryName(destination));
File.Move(source, destination);
Serilog.Log.Logger.Information("File successfully moved", new { source, destination });
}
}
catch (Exception e)
{
Serilog.Log.Logger.Error(e, "Failed to move file", new { source, destination });
throw;
}
});
}
}

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,108 +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;
// book info
private string title;
private string authorNames;
private string narratorNames;
#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, string.Join(", ", libraryBook.Book.Authors));
AudioDecodable_NarratorsDiscovered(sender, string.Join(", ", 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);
}
#endregion
#region AudioDecodable event handlers
public override void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
{
base.AudioDecodable_RequestCoverArt(sender, setCoverArtDelegate);
setCoverArtDelegate(GetCoverArtDelegate?.Invoke());
}
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();
}
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
// thread-safe UI updates
private void updateBookInfo()
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
private void updateRemainingTime(int remaining)
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec");
}
}

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,162 +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_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate) { }
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) { }
#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

@@ -1,65 +0,0 @@
namespace LibationWinForms.Dialogs
{
partial class IndexLibraryDialog
{
/// <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.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(28, 24);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(263, 13);
this.label1.TabIndex = 0;
this.label1.Text = "Scanning Audible library. This may take a few minutes";
//
// IndexLibraryDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(440, 63);
this.Controls.Add(this.label1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "IndexLibraryDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Scan Library";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
}
}

View File

@@ -1,49 +0,0 @@
using ApplicationServices;
using InternalUtilities;
using LibationWinForms.Login;
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class IndexLibraryDialog : Form
{
private Account[] _accounts { get; }
public int NewBooksAdded { get; private set; }
public int TotalBooksProcessed { get; private set; }
public IndexLibraryDialog(params Account[] accounts)
{
_accounts = accounts;
InitializeComponent();
this.Shown += IndexLibraryDialog_Shown;
}
private async void IndexLibraryDialog_Shown(object sender, EventArgs e)
{
if (_accounts != null && _accounts.Length > 0)
{
this.label1.Text
= (_accounts.Length == 1)
? "Scanning Audible library. This may take a few minutes."
: $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account.";
try
{
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((account) => ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account)), _accounts);
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show(
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library",
ex);
}
}
this.Close();
}
}
}

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,179 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class LoginExternalDialog
{
/// <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(LoginExternalDialog));
this.submitBtn = new System.Windows.Forms.Button();
this.localeLbl = new System.Windows.Forms.Label();
this.usernameLbl = new System.Windows.Forms.Label();
this.loginUrlLbl = new System.Windows.Forms.Label();
this.loginUrlTb = new System.Windows.Forms.TextBox();
this.copyBtn = new System.Windows.Forms.Button();
this.launchBrowserBtn = new System.Windows.Forms.Button();
this.instructionsLbl = new System.Windows.Forms.Label();
this.responseUrlTb = new System.Windows.Forms.TextBox();
this.SuspendLayout();
//
// submitBtn
//
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.submitBtn.Location = new System.Drawing.Point(665, 400);
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.submitBtn.Name = "submitBtn";
this.submitBtn.Size = new System.Drawing.Size(88, 27);
this.submitBtn.TabIndex = 8;
this.submitBtn.Text = "Submit";
this.submitBtn.UseVisualStyleBackColor = true;
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
//
// localeLbl
//
this.localeLbl.AutoSize = true;
this.localeLbl.Location = new System.Drawing.Point(14, 10);
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.localeLbl.Name = "localeLbl";
this.localeLbl.Size = new System.Drawing.Size(61, 15);
this.localeLbl.TabIndex = 0;
this.localeLbl.Text = "Locale: {0}";
//
// usernameLbl
//
this.usernameLbl.AutoSize = true;
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.usernameLbl.Name = "usernameLbl";
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
this.usernameLbl.TabIndex = 1;
this.usernameLbl.Text = "Username: {0}";
//
// loginUrlLbl
//
this.loginUrlLbl.AutoSize = true;
this.loginUrlLbl.Location = new System.Drawing.Point(14, 61);
this.loginUrlLbl.Name = "loginUrlLbl";
this.loginUrlLbl.Size = new System.Drawing.Size(180, 15);
this.loginUrlLbl.TabIndex = 2;
this.loginUrlLbl.Text = "Paste this URL into your browser:";
//
// loginUrlTb
//
this.loginUrlTb.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.loginUrlTb.Location = new System.Drawing.Point(14, 79);
this.loginUrlTb.Multiline = true;
this.loginUrlTb.Name = "loginUrlTb";
this.loginUrlTb.ReadOnly = true;
this.loginUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.loginUrlTb.Size = new System.Drawing.Size(739, 92);
this.loginUrlTb.TabIndex = 3;
//
// copyBtn
//
this.copyBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.copyBtn.Location = new System.Drawing.Point(14, 177);
this.copyBtn.Name = "copyBtn";
this.copyBtn.Size = new System.Drawing.Size(165, 23);
this.copyBtn.TabIndex = 4;
this.copyBtn.Text = "Copy URL to clipboard";
this.copyBtn.UseVisualStyleBackColor = true;
this.copyBtn.Click += new System.EventHandler(this.copyBtn_Click);
//
// launchBrowserBtn
//
this.launchBrowserBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.launchBrowserBtn.Location = new System.Drawing.Point(588, 177);
this.launchBrowserBtn.Name = "launchBrowserBtn";
this.launchBrowserBtn.Size = new System.Drawing.Size(165, 23);
this.launchBrowserBtn.TabIndex = 5;
this.launchBrowserBtn.Text = "Launch in browser";
this.launchBrowserBtn.UseVisualStyleBackColor = true;
this.launchBrowserBtn.Click += new System.EventHandler(this.launchBrowserBtn_Click);
//
// instructionsLbl
//
this.instructionsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.instructionsLbl.AutoSize = true;
this.instructionsLbl.Location = new System.Drawing.Point(14, 203);
this.instructionsLbl.Name = "instructionsLbl";
this.instructionsLbl.Size = new System.Drawing.Size(436, 90);
this.instructionsLbl.TabIndex = 6;
this.instructionsLbl.Text = resources.GetString("instructionsLbl.Text");
//
// responseUrlTb
//
this.responseUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.responseUrlTb.Location = new System.Drawing.Point(14, 296);
this.responseUrlTb.Multiline = true;
this.responseUrlTb.Name = "responseUrlTb";
this.responseUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.responseUrlTb.Size = new System.Drawing.Size(739, 98);
this.responseUrlTb.TabIndex = 7;
//
// LoginExternalDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(766, 440);
this.Controls.Add(this.responseUrlTb);
this.Controls.Add(this.instructionsLbl);
this.Controls.Add(this.launchBrowserBtn);
this.Controls.Add(this.copyBtn);
this.Controls.Add(this.loginUrlTb);
this.Controls.Add(this.loginUrlLbl);
this.Controls.Add(this.usernameLbl);
this.Controls.Add(this.localeLbl);
this.Controls.Add(this.submitBtn);
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 = "LoginExternalDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Audible External Login";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label localeLbl;
private System.Windows.Forms.Label usernameLbl;
private System.Windows.Forms.Label loginUrlLbl;
private System.Windows.Forms.TextBox loginUrlTb;
private System.Windows.Forms.Button copyBtn;
private System.Windows.Forms.Button launchBrowserBtn;
private System.Windows.Forms.Label instructionsLbl;
private System.Windows.Forms.TextBox responseUrlTb;
}
}

View File

@@ -1,379 +0,0 @@
namespace LibationWinForms.Dialogs
{
partial class SettingsDialog
{
/// <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.booksLocationDescLbl = new System.Windows.Forms.Label();
this.inProgressDescLbl = new System.Windows.Forms.Label();
this.saveBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
this.importEpisodesCb = new System.Windows.Forms.CheckBox();
this.downloadEpisodesCb = new System.Windows.Forms.CheckBox();
this.badBookGb = new System.Windows.Forms.GroupBox();
this.badBookIgnoreRb = new System.Windows.Forms.RadioButton();
this.badBookRetryRb = new System.Windows.Forms.RadioButton();
this.badBookAbortRb = new System.Windows.Forms.RadioButton();
this.badBookAskRb = new System.Windows.Forms.RadioButton();
this.decryptAndConvertGb = new System.Windows.Forms.GroupBox();
this.splitFilesByChapterCbox = new System.Windows.Forms.CheckBox();
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
this.convertLossyRb = new System.Windows.Forms.RadioButton();
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
this.logsBtn = new System.Windows.Forms.Button();
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
this.booksGb = new System.Windows.Forms.GroupBox();
this.loggingLevelLbl = new System.Windows.Forms.Label();
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
this.advancedSettingsGb.SuspendLayout();
this.badBookGb.SuspendLayout();
this.decryptAndConvertGb.SuspendLayout();
this.booksGb.SuspendLayout();
this.SuspendLayout();
//
// booksLocationDescLbl
//
this.booksLocationDescLbl.AutoSize = true;
this.booksLocationDescLbl.Location = new System.Drawing.Point(7, 19);
this.booksLocationDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
this.booksLocationDescLbl.Size = new System.Drawing.Size(69, 15);
this.booksLocationDescLbl.TabIndex = 1;
this.booksLocationDescLbl.Text = "[book desc]";
//
// inProgressDescLbl
//
this.inProgressDescLbl.AutoSize = true;
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 199);
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.inProgressDescLbl.Name = "inProgressDescLbl";
this.inProgressDescLbl.Size = new System.Drawing.Size(43, 45);
this.inProgressDescLbl.TabIndex = 18;
this.inProgressDescLbl.Text = "[desc]\r\n[line 2]\r\n[line 3]";
//
// 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(714, 496);
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 = 98;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(832, 496);
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 99;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// advancedSettingsGb
//
this.advancedSettingsGb.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.advancedSettingsGb.Controls.Add(this.importEpisodesCb);
this.advancedSettingsGb.Controls.Add(this.downloadEpisodesCb);
this.advancedSettingsGb.Controls.Add(this.badBookGb);
this.advancedSettingsGb.Controls.Add(this.decryptAndConvertGb);
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
this.advancedSettingsGb.Controls.Add(this.inProgressDescLbl);
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 176);
this.advancedSettingsGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.advancedSettingsGb.Name = "advancedSettingsGb";
this.advancedSettingsGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 309);
this.advancedSettingsGb.TabIndex = 6;
this.advancedSettingsGb.TabStop = false;
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
//
// importEpisodesCb
//
this.importEpisodesCb.AutoSize = true;
this.importEpisodesCb.Location = new System.Drawing.Point(7, 22);
this.importEpisodesCb.Name = "importEpisodesCb";
this.importEpisodesCb.Size = new System.Drawing.Size(146, 19);
this.importEpisodesCb.TabIndex = 7;
this.importEpisodesCb.Text = "[import episodes desc]";
this.importEpisodesCb.UseVisualStyleBackColor = true;
//
// downloadEpisodesCb
//
this.downloadEpisodesCb.AutoSize = true;
this.downloadEpisodesCb.Location = new System.Drawing.Point(7, 47);
this.downloadEpisodesCb.Name = "downloadEpisodesCb";
this.downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
this.downloadEpisodesCb.TabIndex = 8;
this.downloadEpisodesCb.Text = "[download episodes desc]";
this.downloadEpisodesCb.UseVisualStyleBackColor = true;
//
// badBookGb
//
this.badBookGb.Controls.Add(this.badBookIgnoreRb);
this.badBookGb.Controls.Add(this.badBookRetryRb);
this.badBookGb.Controls.Add(this.badBookAbortRb);
this.badBookGb.Controls.Add(this.badBookAskRb);
this.badBookGb.Location = new System.Drawing.Point(372, 72);
this.badBookGb.Name = "badBookGb";
this.badBookGb.Size = new System.Drawing.Size(529, 124);
this.badBookGb.TabIndex = 13;
this.badBookGb.TabStop = false;
this.badBookGb.Text = "[bad book desc]";
//
// badBookIgnoreRb
//
this.badBookIgnoreRb.AutoSize = true;
this.badBookIgnoreRb.Location = new System.Drawing.Point(6, 97);
this.badBookIgnoreRb.Name = "badBookIgnoreRb";
this.badBookIgnoreRb.Size = new System.Drawing.Size(94, 19);
this.badBookIgnoreRb.TabIndex = 17;
this.badBookIgnoreRb.TabStop = true;
this.badBookIgnoreRb.Text = "[ignore desc]";
this.badBookIgnoreRb.UseVisualStyleBackColor = true;
//
// badBookRetryRb
//
this.badBookRetryRb.AutoSize = true;
this.badBookRetryRb.Location = new System.Drawing.Point(6, 72);
this.badBookRetryRb.Name = "badBookRetryRb";
this.badBookRetryRb.Size = new System.Drawing.Size(84, 19);
this.badBookRetryRb.TabIndex = 16;
this.badBookRetryRb.TabStop = true;
this.badBookRetryRb.Text = "[retry desc]";
this.badBookRetryRb.UseVisualStyleBackColor = true;
//
// badBookAbortRb
//
this.badBookAbortRb.AutoSize = true;
this.badBookAbortRb.Location = new System.Drawing.Point(6, 47);
this.badBookAbortRb.Name = "badBookAbortRb";
this.badBookAbortRb.Size = new System.Drawing.Size(88, 19);
this.badBookAbortRb.TabIndex = 15;
this.badBookAbortRb.TabStop = true;
this.badBookAbortRb.Text = "[abort desc]";
this.badBookAbortRb.UseVisualStyleBackColor = true;
//
// badBookAskRb
//
this.badBookAskRb.AutoSize = true;
this.badBookAskRb.Location = new System.Drawing.Point(6, 22);
this.badBookAskRb.Name = "badBookAskRb";
this.badBookAskRb.Size = new System.Drawing.Size(77, 19);
this.badBookAskRb.TabIndex = 14;
this.badBookAskRb.TabStop = true;
this.badBookAskRb.Text = "[ask desc]";
this.badBookAskRb.UseVisualStyleBackColor = true;
//
// decryptAndConvertGb
//
this.decryptAndConvertGb.Controls.Add(this.splitFilesByChapterCbox);
this.decryptAndConvertGb.Controls.Add(this.allowLibationFixupCbox);
this.decryptAndConvertGb.Controls.Add(this.convertLossyRb);
this.decryptAndConvertGb.Controls.Add(this.convertLosslessRb);
this.decryptAndConvertGb.Location = new System.Drawing.Point(7, 72);
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
this.decryptAndConvertGb.TabIndex = 9;
this.decryptAndConvertGb.TabStop = false;
this.decryptAndConvertGb.Text = "Decrypt and convert";
//
// splitFilesByChapterCbox
//
this.splitFilesByChapterCbox.AutoSize = true;
this.splitFilesByChapterCbox.Location = new System.Drawing.Point(6, 46);
this.splitFilesByChapterCbox.Name = "splitFilesByChapterCbox";
this.splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19);
this.splitFilesByChapterCbox.TabIndex = 13;
this.splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
this.splitFilesByChapterCbox.UseVisualStyleBackColor = true;
//
// allowLibationFixupCbox
//
this.allowLibationFixupCbox.AutoSize = true;
this.allowLibationFixupCbox.Checked = true;
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
this.allowLibationFixupCbox.Location = new System.Drawing.Point(6, 22);
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
this.allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19);
this.allowLibationFixupCbox.TabIndex = 10;
this.allowLibationFixupCbox.Text = "[AllowLibationFixup desc]";
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
//
// convertLossyRb
//
this.convertLossyRb.AutoSize = true;
this.convertLossyRb.Location = new System.Drawing.Point(6, 101);
this.convertLossyRb.Name = "convertLossyRb";
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
this.convertLossyRb.TabIndex = 12;
this.convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
this.convertLossyRb.UseVisualStyleBackColor = true;
//
// convertLosslessRb
//
this.convertLosslessRb.AutoSize = true;
this.convertLosslessRb.Checked = true;
this.convertLosslessRb.Location = new System.Drawing.Point(6, 76);
this.convertLosslessRb.Name = "convertLosslessRb";
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
this.convertLosslessRb.TabIndex = 11;
this.convertLosslessRb.TabStop = true;
this.convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
this.convertLosslessRb.UseVisualStyleBackColor = true;
//
// inProgressSelectControl
//
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 247);
this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.inProgressSelectControl.Name = "inProgressSelectControl";
this.inProgressSelectControl.Size = new System.Drawing.Size(894, 52);
this.inProgressSelectControl.TabIndex = 19;
//
// logsBtn
//
this.logsBtn.Location = new System.Drawing.Point(262, 147);
this.logsBtn.Name = "logsBtn";
this.logsBtn.Size = new System.Drawing.Size(132, 23);
this.logsBtn.TabIndex = 5;
this.logsBtn.Text = "Open log folder";
this.logsBtn.UseVisualStyleBackColor = true;
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
//
// booksSelectControl
//
this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.booksSelectControl.Name = "booksSelectControl";
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
this.booksSelectControl.TabIndex = 2;
//
// booksGb
//
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.booksGb.Controls.Add(this.booksSelectControl);
this.booksGb.Controls.Add(this.booksLocationDescLbl);
this.booksGb.Location = new System.Drawing.Point(12, 12);
this.booksGb.Name = "booksGb";
this.booksGb.Size = new System.Drawing.Size(908, 129);
this.booksGb.TabIndex = 0;
this.booksGb.TabStop = false;
this.booksGb.Text = "Books location";
//
// loggingLevelLbl
//
this.loggingLevelLbl.AutoSize = true;
this.loggingLevelLbl.Location = new System.Drawing.Point(12, 150);
this.loggingLevelLbl.Name = "loggingLevelLbl";
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
this.loggingLevelLbl.TabIndex = 3;
this.loggingLevelLbl.Text = "Logging level";
//
// loggingLevelCb
//
this.loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.loggingLevelCb.FormattingEnabled = true;
this.loggingLevelCb.Location = new System.Drawing.Point(96, 147);
this.loggingLevelCb.Name = "loggingLevelCb";
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
this.loggingLevelCb.TabIndex = 4;
//
// SettingsDialog
//
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(933, 539);
this.Controls.Add(this.logsBtn);
this.Controls.Add(this.loggingLevelCb);
this.Controls.Add(this.loggingLevelLbl);
this.Controls.Add(this.booksGb);
this.Controls.Add(this.advancedSettingsGb);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "SettingsDialog";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Settings";
this.Load += new System.EventHandler(this.SettingsDialog_Load);
this.advancedSettingsGb.ResumeLayout(false);
this.advancedSettingsGb.PerformLayout();
this.badBookGb.ResumeLayout(false);
this.badBookGb.PerformLayout();
this.decryptAndConvertGb.ResumeLayout(false);
this.decryptAndConvertGb.PerformLayout();
this.booksGb.ResumeLayout(false);
this.booksGb.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label booksLocationDescLbl;
private System.Windows.Forms.Label inProgressDescLbl;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.GroupBox advancedSettingsGb;
private System.Windows.Forms.CheckBox allowLibationFixupCbox;
private DirectoryOrCustomSelectControl booksSelectControl;
private DirectorySelectControl inProgressSelectControl;
private System.Windows.Forms.RadioButton convertLossyRb;
private System.Windows.Forms.RadioButton convertLosslessRb;
private System.Windows.Forms.GroupBox booksGb;
private System.Windows.Forms.Button logsBtn;
private System.Windows.Forms.Label loggingLevelLbl;
private System.Windows.Forms.ComboBox loggingLevelCb;
private System.Windows.Forms.GroupBox decryptAndConvertGb;
private System.Windows.Forms.GroupBox badBookGb;
private System.Windows.Forms.RadioButton badBookRetryRb;
private System.Windows.Forms.RadioButton badBookAbortRb;
private System.Windows.Forms.RadioButton badBookAskRb;
private System.Windows.Forms.RadioButton badBookIgnoreRb;
private System.Windows.Forms.CheckBox downloadEpisodesCb;
private System.Windows.Forms.CheckBox importEpisodesCb;
private System.Windows.Forms.CheckBox splitFilesByChapterCbox;
}
}

View File

@@ -1,158 +0,0 @@
using System;
using System.Linq;
using System.IO;
using System.Windows.Forms;
using Dinah.Core;
using LibationFileManager;
namespace LibationWinForms.Dialogs
{
public partial class SettingsDialog : Form
{
private Configuration config { get; } = Configuration.Instance;
private Func<string, string> desc { get; } = Configuration.GetDescription;
public SettingsDialog() => InitializeComponent();
private void SettingsDialog_Load(object sender, EventArgs e)
{
if (this.DesignMode)
return;
{
loggingLevelCb.Items.Clear();
foreach (var level in Enum<Serilog.Events.LogEventLevel>.GetValues())
loggingLevelCb.Items.Add(level);
loggingLevelCb.SelectedItem = config.LogLevel;
}
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
this.allowLibationFixupCbox.Text = desc(nameof(config.AllowLibationFixup));
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
booksSelectControl.SetSearchTitle("books location");
booksSelectControl.SetDirectoryItems(
new()
{
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs
},
Configuration.KnownDirectories.UserProfile,
"Books");
booksSelectControl.SelectDirectory(config.Books);
importEpisodesCb.Checked = config.ImportEpisodes;
downloadEpisodesCb.Checked = config.DownloadEpisodes;
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
convertLosslessRb.Checked = !config.DecryptToLossy;
convertLossyRb.Checked = config.DecryptToLossy;
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
allowLibationFixupCbox_CheckedChanged(this, e);
inProgressSelectControl.SetDirectoryItems(new()
{
Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles
}, Configuration.KnownDirectories.WinTemp);
inProgressSelectControl.SelectDirectory(config.InProgress);
badBookGb.Text = desc(nameof(config.BadBook));
badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription();
badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription();
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
var rb = config.BadBook switch
{
Configuration.BadBookAction.Ask => this.badBookAskRb,
Configuration.BadBookAction.Abort => this.badBookAbortRb,
Configuration.BadBookAction.Retry => this.badBookRetryRb,
Configuration.BadBookAction.Ignore => this.badBookIgnoreRb,
_ => this.badBookAskRb
};
rb.Checked = true;
}
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
{
convertLosslessRb.Enabled = allowLibationFixupCbox.Checked;
convertLossyRb.Enabled = allowLibationFixupCbox.Checked;
splitFilesByChapterCbox.Enabled = allowLibationFixupCbox.Checked;
if (!allowLibationFixupCbox.Checked)
{
convertLosslessRb.Checked = true;
splitFilesByChapterCbox.Checked = false;
}
}
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(Configuration.Instance.LibationFiles);
private void saveBtn_Click(object sender, EventArgs e)
{
var newBooks = booksSelectControl.SelectedDirectory;
if (string.IsNullOrWhiteSpace(newBooks))
{
MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (!Directory.Exists(newBooks))
{
if (booksSelectControl.SelectedDirectoryIsCustom)
{
MessageBox.Show($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (booksSelectControl.SelectedDirectoryIsKnown)
Directory.CreateDirectory(newBooks);
}
config.Books = newBooks;
{
var logLevelOld = config.LogLevel;
var logLevelNew = (Serilog.Events.LogEventLevel)loggingLevelCb.SelectedItem;
config.LogLevel = logLevelNew;
// 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();
}
config.ImportEpisodes = importEpisodesCb.Checked;
config.DownloadEpisodes = downloadEpisodesCb.Checked;
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
config.DecryptToLossy = convertLossyRb.Checked;
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
config.InProgress = inProgressSelectControl.SelectedDirectory;
config.BadBook
= badBookAskRb.Checked ? Configuration.BadBookAction.Ask
: badBookAbortRb.Checked ? Configuration.BadBookAction.Abort
: badBookRetryRb.Checked ? Configuration.BadBookAction.Retry
: badBookIgnoreRb.Checked ? Configuration.BadBookAction.Ignore
: Configuration.BadBookAction.Ask;
this.DialogResult = DialogResult.OK;
this.Close();
}
private void cancelBtn_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
}

View File

@@ -1,401 +0,0 @@
namespace LibationWinForms
{
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.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.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.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.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.importToolStripMenuItem,
this.liberateToolStripMenuItem,
this.exportToolStripMenuItem,
this.quickFiltersToolStripMenuItem,
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(1007, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
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);
//
// 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(180, 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(180, 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.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.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.accountsToolStripMenuItem,
this.basicSettingsToolStripMenuItem});
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);
//
// statusStrip1
//
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();
}
#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 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 System.Windows.Forms.TextBox filterSearchTb;
private System.Windows.Forms.Button filterBtn;
private System.Windows.Forms.Button filterHelpBtn;
private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem;
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.ToolStripMenuItem editQuickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem basicSettingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem accountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfAllAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfSomeAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem noAccountsYetAddAccountToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem convertAllM4bToMp3ToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeLibraryBooksToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeAllAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeSomeAccountsToolStripMenuItem;
}
}

View File

@@ -1,456 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using ApplicationServices;
using Dinah.Core;
using Dinah.Core.Drawing;
using Dinah.Core.Threading;
using InternalUtilities;
using LibationFileManager;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
public partial class Form1 : Form
{
private string beginBookBackupsToolStripMenuItem_format { get; }
private string beginPdfBackupsToolStripMenuItem_format { get; }
public Form1()
{
InitializeComponent();
// back up string formats
beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text;
beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text;
if (this.DesignMode)
return;
// independent UI updates
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
this.Load += RefreshImportMenu;
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
LibraryCommands.LibrarySizeChanged += reloadGridAndUpdateBottomNumbers;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
// used by async migrations to update ui when complete
DataLayer.UserDefinedItem.Batch_ItemChanged += reloadGridAndUpdateBottomNumbers;
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
}
private void Form1_Load(object sender, EventArgs e)
{
if (this.DesignMode)
return;
// can't refactor into "this.Load => reloadGridAndUpdateBottomNumbers"
// because loadInitialQuickFilterState must follow it
reloadGridAndUpdateBottomNumbers();
// also applies filter. ONLY call AFTER loading grid
loadInitialQuickFilterState();
}
private void reloadGridAndUpdateBottomNumbers(object _ = null, object __ = null)
{
// suppressed filter while init'ing UI
var prev_isProcessingGridSelect = isProcessingGridSelect;
isProcessingGridSelect = true;
this.UIThreadSync(() => setGrid());
isProcessingGridSelect = prev_isProcessingGridSelect;
// UI init complete. now we can apply filter
this.UIThreadAsync(() => doFilter(lastGoodFilter));
setBackupCounts(null, null);
}
#region reload grid
private ProductsGrid currProductsGrid;
private void setGrid()
{
SuspendLayout();
{
if (currProductsGrid != null)
{
gridPanel.Controls.Remove(currProductsGrid);
currProductsGrid.VisibleCountChanged -= setVisibleCount;
currProductsGrid.Dispose();
}
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
currProductsGrid.VisibleCountChanged += setVisibleCount;
gridPanel.UIThreadSync(() => gridPanel.Controls.Add(currProductsGrid));
currProductsGrid.Display();
}
ResumeLayout();
}
#endregion
#region bottom: qty books visible
private void setVisibleCount(object _, int qty) => visibleCountLbl.Text = string.Format("Visible: {0}", qty);
#endregion
#region bottom: backup counts
private System.ComponentModel.BackgroundWorker updateCountsBw;
private bool runBackupCountsAgain;
private void setBackupCounts(object _, object __)
{
runBackupCountsAgain = true;
if (updateCountsBw is not null)
return;
updateCountsBw = new System.ComponentModel.BackgroundWorker();
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_RunWorkerCompleted;
updateCountsBw.RunWorkerAsync();
}
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
while (runBackupCountsAgain)
{
runBackupCountsAgain = false;
var libraryStats = LibraryCommands.GetCounts();
e.Result = libraryStats;
}
updateCountsBw = null;
}
private void UpdateCountsBw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
setBookBackupCounts(libraryStats);
setPdfBackupCounts(libraryStats);
}
private void setBookBackupCounts(LibraryCommands.LibraryStats libraryStats)
{
var backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
// enable/disable export
var hasResults = 0 < (libraryStats.booksFullyBackedUp + libraryStats.booksDownloadedOnly + libraryStats.booksNoProgress + libraryStats.booksError);
exportLibraryToolStripMenuItem.Enabled = hasResults;
// update bottom numbers
var pending = libraryStats.booksNoProgress + libraryStats.booksDownloadedOnly;
var statusStripText
= !hasResults ? "No books. Begin by importing your library"
: libraryStats.booksError > 0 ? string.Format(backupsCountsLbl_Format + " Errors: {3}", libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp, libraryStats.booksError)
: pending > 0 ? string.Format(backupsCountsLbl_Format, libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp)
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
// update menu item
var menuItemText
= pending > 0
? $"{pending} remaining"
: "All books have been liberated";
// update UI
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
}
private void setPdfBackupCounts(LibraryCommands.LibraryStats libraryStats)
{
var pdfsCountsLbl_Format = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
// update bottom numbers
var hasResults = 0 < (libraryStats.pdfsNotDownloaded + libraryStats.pdfsDownloaded);
var statusStripText
= !hasResults ? ""
: libraryStats.pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
// update menu item
var menuItemText
= libraryStats.pdfsNotDownloaded > 0
? $"{libraryStats.pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
// update UI
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0);
menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
}
#endregion
#region filter
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
private void AddFilterBtn_Click(object sender, EventArgs e)
{
QuickFilters.Add(this.filterSearchTb.Text);
UpdateFilterDropDown();
}
private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Return)
{
doFilter();
// silence the 'ding'
e.Handled = true;
}
}
private void filterBtn_Click(object sender, EventArgs e) => doFilter();
private bool isProcessingGridSelect = false;
private string lastGoodFilter = "";
private void doFilter(string filterString)
{
this.filterSearchTb.Text = filterString;
doFilter();
}
private void doFilter()
{
if (isProcessingGridSelect || currProductsGrid == null)
return;
try
{
currProductsGrid.Filter(filterSearchTb.Text);
lastGoodFilter = filterSearchTb.Text;
}
catch (Exception ex)
{
MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
// re-apply last good filter
doFilter(lastGoodFilter);
}
}
#endregion
#region Import menu
public void RefreshImportMenu(object _ = null, EventArgs __ = null)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var count = persister.AccountsSettings.Accounts.Count;
noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0;
scanLibraryToolStripMenuItem.Visible = count == 1;
scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1;
scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1;
removeLibraryBooksToolStripMenuItem.Visible = count != 0;
removeSomeAccountsToolStripMenuItem.Visible = count > 1;
removeAllAccountsToolStripMenuItem.Visible = count > 1;
}
private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e)
{
MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
new AccountsDialog(this).ShowDialog();
}
private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
scanLibraries(firstAccount);
}
private void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibraries(allAccounts);
}
private void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog(this);
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibraries(scanAccountsDialog.CheckedAccounts);
}
private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e)
{
// if 0 accounts, this will not be visible
// if 1 account, run scanLibrariesRemovedBooks() on this account
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.GetAll();
if (accounts.Count != 1)
return;
var firstAccount = accounts.Single();
scanLibrariesRemovedBooks(firstAccount);
}
// selectively remove books from all accounts
private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibrariesRemovedBooks(allAccounts.ToArray());
}
// selectively remove books from some accounts
private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog(this);
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
}
private void scanLibrariesRemovedBooks(params Account[] accounts)
{
using var dialog = new RemoveBooksDialog(accounts);
dialog.ShowDialog();
}
private void scanLibraries(IEnumerable<Account> accounts) => scanLibraries(accounts.ToArray());
private void scanLibraries(params Account[] accounts)
{
using var dialog = new IndexLibraryDialog(accounts);
dialog.ShowDialog();
var totalProcessed = dialog.TotalBooksProcessed;
var newAdded = dialog.NewBooksAdded;
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
#endregion
#region liberate menu
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync();
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync();
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
{
var result = MessageBox.Show(
"This converts all m4b titles in your library to mp3 files. Original files are not deleted."
+ "\r\nFor large libraries this will take a long time and will take up more disk space."
+ "\r\n\r\nContinue?"
+ "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)",
"Convert all M4b => Mp3?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
}
#endregion
#region Export menu
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)
{
MessageBoxAlertAdmin.Show("Error attempting to export your library.", "Error exporting", ex);
}
}
#endregion
#region quick filters menu
private void loadInitialQuickFilterState()
{
// set inital state. do once only
firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault;
// load default filter. do once only
if (QuickFilters.UseDefault)
doFilter(QuickFilters.Filters.FirstOrDefault());
// do after every save
UpdateFilterDropDown();
}
private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e)
{
firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked;
QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked;
}
private object quickFilterTag { get; } = new object();
public void UpdateFilterDropDown()
{
// remove old
for (var i = quickFiltersToolStripMenuItem.DropDownItems.Count - 1; i >= 0; i--)
{
var menuItem = quickFiltersToolStripMenuItem.DropDownItems[i];
if (menuItem.Tag == quickFilterTag)
quickFiltersToolStripMenuItem.DropDownItems.Remove(menuItem);
}
// re-populate
var index = 0;
foreach (var filter in QuickFilters.Filters)
{
var menuItem = new ToolStripMenuItem
{
Tag = quickFilterTag,
Text = $"&{++index}: {filter}"
};
menuItem.Click += (_, __) => doFilter(filter);
quickFiltersToolStripMenuItem.DropDownItems.Add(menuItem);
}
}
private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters(this).ShowDialog();
#endregion
#region settings menu
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog();
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
#endregion
}
}

View File

@@ -1,27 +0,0 @@
using System;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
public static class MessageBoxAlertAdmin
{
/// <summary>
/// Logs error. Displays a message box dialog with specified text and caption.
/// </summary>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="exception">Exception to log</param>
/// <returns>One of the System.Windows.Forms.DialogResult values.</returns>
public static System.Windows.Forms.DialogResult Show(string text, string caption, Exception exception)
{
try
{
Serilog.Log.Logger.Error(exception, "Alert admin error: {@DebugText}", new { text, caption });
}
catch { }
using var form = new MessageBoxAlertAdminDialog(text, caption, exception);
return form.ShowDialog();
}
}
}

View File

@@ -1,28 +0,0 @@
using System;
using System.Linq;
using Dinah.Core.Logging;
using Serilog;
using System.Windows.Forms;
namespace LibationWinForms
{
public static class MessageBoxVerboseLoggingWarning
{
public static void ShowIfTrue()
{
// when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured
if (Log.Logger.IsVerboseEnabled())
MessageBox.Show(@"
Warning: verbose logging is enabled.
This should be used for debugging only. It creates many
more logs and debug files, neither of which are as
strictly anonymous.
When you are finished debugging, it's highly recommended
to set your debug MinimumLevel to Information and restart
Libation.
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
}

View File

@@ -1,242 +0,0 @@
namespace LibationWinForms
{
partial class ProductsGrid
{
/// <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 Component 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.components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
this.gridEntryBindingSource = new LibationWinForms.SyncBindingSource(this.components);
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.dataGridViewImageButtonBoxColumn1 = new LibationWinForms.LiberateDataGridViewImageButtonColumn();
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewImageButtonBoxColumn2 = new LibationWinForms.EditTagsDataGridViewImageButtonColumn();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
this.SuspendLayout();
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.GridEntry);
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AllowUserToAddRows = false;
this.gridEntryDataGridView.AllowUserToDeleteRows = false;
this.gridEntryDataGridView.AllowUserToResizeRows = false;
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.dataGridViewImageButtonBoxColumn1,
this.dataGridViewImageColumn1,
this.dataGridViewTextBoxColumn1,
this.dataGridViewTextBoxColumn2,
this.dataGridViewTextBoxColumn3,
this.dataGridViewTextBoxColumn4,
this.dataGridViewTextBoxColumn5,
this.dataGridViewTextBoxColumn6,
this.dataGridViewTextBoxColumn7,
this.dataGridViewTextBoxColumn8,
this.dataGridViewTextBoxColumn9,
this.dataGridViewTextBoxColumn10,
this.dataGridViewTextBoxColumn11,
this.dataGridViewImageButtonBoxColumn2});
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
this.gridEntryDataGridView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.ReadOnly = true;
this.gridEntryDataGridView.RowHeadersVisible = false;
this.gridEntryDataGridView.RowTemplate.Height = 82;
this.gridEntryDataGridView.Size = new System.Drawing.Size(1510, 380);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageButtonBoxColumn1
//
this.dataGridViewImageButtonBoxColumn1.DataPropertyName = "Liberate";
this.dataGridViewImageButtonBoxColumn1.HeaderText = "Liberate";
this.dataGridViewImageButtonBoxColumn1.Name = "dataGridViewImageButtonBoxColumn1";
this.dataGridViewImageButtonBoxColumn1.ReadOnly = true;
this.dataGridViewImageButtonBoxColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.dataGridViewImageButtonBoxColumn1.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.dataGridViewImageButtonBoxColumn1.Width = 75;
//
// dataGridViewImageColumn1
//
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
this.dataGridViewImageColumn1.HeaderText = "Cover";
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
this.dataGridViewImageColumn1.ReadOnly = true;
this.dataGridViewImageColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.dataGridViewImageColumn1.ToolTipText = "Cover Art";
this.dataGridViewImageColumn1.Width = 80;
//
// dataGridViewTextBoxColumn1
//
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
this.dataGridViewTextBoxColumn1.ReadOnly = true;
this.dataGridViewTextBoxColumn1.Width = 200;
//
// dataGridViewTextBoxColumn2
//
this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors";
this.dataGridViewTextBoxColumn2.HeaderText = "Authors";
this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2";
this.dataGridViewTextBoxColumn2.ReadOnly = true;
//
// dataGridViewTextBoxColumn3
//
this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators";
this.dataGridViewTextBoxColumn3.HeaderText = "Narrators";
this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3";
this.dataGridViewTextBoxColumn3.ReadOnly = true;
//
// dataGridViewTextBoxColumn4
//
this.dataGridViewTextBoxColumn4.DataPropertyName = "Length";
this.dataGridViewTextBoxColumn4.HeaderText = "Length";
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
this.dataGridViewTextBoxColumn4.ReadOnly = true;
this.dataGridViewTextBoxColumn4.ToolTipText = "Recording Length";
//
// dataGridViewTextBoxColumn5
//
this.dataGridViewTextBoxColumn5.DataPropertyName = "Series";
this.dataGridViewTextBoxColumn5.HeaderText = "Series";
this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5";
this.dataGridViewTextBoxColumn5.ReadOnly = true;
//
// dataGridViewTextBoxColumn6
//
this.dataGridViewTextBoxColumn6.DataPropertyName = "Description";
this.dataGridViewTextBoxColumn6.HeaderText = "Description";
this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6";
this.dataGridViewTextBoxColumn6.ReadOnly = true;
//
// dataGridViewTextBoxColumn7
//
this.dataGridViewTextBoxColumn7.DataPropertyName = "Category";
this.dataGridViewTextBoxColumn7.HeaderText = "Category";
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
this.dataGridViewTextBoxColumn7.ReadOnly = true;
//
// ProductRating
//
this.dataGridViewTextBoxColumn8.DataPropertyName = "ProductRating";
this.dataGridViewTextBoxColumn8.HeaderText = "Product Rating";
this.dataGridViewTextBoxColumn8.Name = "ProductRating";
this.dataGridViewTextBoxColumn8.ReadOnly = true;
this.dataGridViewTextBoxColumn8.Width = 108;
//
// PurchaseDate
//
this.dataGridViewTextBoxColumn9.DataPropertyName = "PurchaseDate";
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase Date";
this.dataGridViewTextBoxColumn9.Name = "PurchaseDate";
this.dataGridViewTextBoxColumn9.ReadOnly = true;
//
// MyRating
//
this.dataGridViewTextBoxColumn10.DataPropertyName = "MyRating";
this.dataGridViewTextBoxColumn10.HeaderText = "My Rating";
this.dataGridViewTextBoxColumn10.Name = "MyRating";
this.dataGridViewTextBoxColumn10.ReadOnly = true;
this.dataGridViewTextBoxColumn10.Width = 108;
//
// dataGridViewTextBoxColumn11
//
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
this.dataGridViewTextBoxColumn11.ReadOnly = true;
this.dataGridViewTextBoxColumn11.Width = 135;
//
// dataGridViewImageButtonBoxColumn2
//
this.dataGridViewImageButtonBoxColumn2.DataPropertyName = "DisplayTags";
this.dataGridViewImageButtonBoxColumn2.HeaderText = "Tags and Details";
this.dataGridViewImageButtonBoxColumn2.Name = "dataGridViewImageButtonBoxColumn2";
this.dataGridViewImageButtonBoxColumn2.ReadOnly = true;
this.dataGridViewImageButtonBoxColumn2.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.dataGridViewImageButtonBoxColumn2.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.gridEntryDataGridView);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(1510, 380);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);
}
#endregion
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
private System.Windows.Forms.DataGridView gridEntryDataGridView;
private LiberateDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn1;
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
private EditTagsDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn2;
}
}

View File

@@ -1,192 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using Dinah.Core;
using Dinah.Core.DataBinding;
using Dinah.Core.Threading;
using Dinah.Core.Windows.Forms;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
// INSTRUCTIONS TO UPDATE DATA_GRID_VIEW
// - delete current DataGridView
// - view > other windows > data sources
// - refresh
// OR
// - Add New Data Source
// Object. Next
// LibationWinForms
// AudibleDTO
// GridEntry
// - go to Design view
// - click on Data Sources > ProductItem. dropdown: DataGridView
// - drag/drop ProductItem on design surface
// AS OF AUGUST 2021 THIS DOES NOT WORK IN VS2019 WITH .NET-5 PROJECTS
public partial class ProductsGrid : UserControl
{
public event EventHandler<int> VisibleCountChanged;
// alias
private DataGridView _dataGridView => gridEntryDataGridView;
public ProductsGrid()
{
InitializeComponent();
// sorting breaks filters. must reapply filters after sorting
_dataGridView.Sorted += (_, __) => Filter();
_dataGridView.CellContentClick += DataGridView_CellContentClick;
EnableDoubleBuffering();
}
private void EnableDoubleBuffering()
{
var propertyInfo = _dataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
propertyInfo.SetValue(_dataGridView, true, null);
}
#region Button controls
private async void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
// handle grid button click: https://stackoverflow.com/a/13687844
if (e.RowIndex < 0 || _dataGridView.Columns[e.ColumnIndex] is not DataGridViewButtonColumn)
return;
var liveGridEntry = getGridEntry(e.RowIndex);
switch (_dataGridView.Columns[e.ColumnIndex].DataPropertyName)
{
case nameof(liveGridEntry.Liberate):
await Liberate_Click(liveGridEntry);
break;
case nameof(liveGridEntry.DisplayTags):
Details_Click(liveGridEntry);
break;
}
}
private async Task Liberate_Click(GridEntry liveGridEntry)
{
var libraryBook = liveGridEntry.LibraryBook;
// liberated: open explorer to file
if (libraryBook.Book.Audio_Exists)
{
var filePath = LibationFileManager.AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
}
return;
}
// else: liberate
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook);
}
private void Details_Click(GridEntry liveGridEntry)
{
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
return;
liveGridEntry.BeginEdit();
liveGridEntry.DisplayTags = bookDetailsForm.NewTags;
liveGridEntry.Liberate = (bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
liveGridEntry.EndEdit();
}
#endregion
#region UI display functions
private bool hasBeenDisplayed = false;
public void Display()
{
if (hasBeenDisplayed)
return;
hasBeenDisplayed = true;
//
// transform into sorted GridEntry.s BEFORE binding
//
var lib = DbContexts.GetLibrary_Flat_NoTracking();
// if no data. hide all columns. return
if (!lib.Any())
{
for (var i = _dataGridView.ColumnCount - 1; i >= 0; i--)
_dataGridView.Columns.RemoveAt(i);
return;
}
var orderedGridEntries = lib
.Select(lb =>
{
var entry = new GridEntry(lb);
entry.Committed += (_, __) => Filter();
return entry;
}).ToList()
// default load order
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
//// more advanced example: sort by author, then series, then title
//.OrderBy(ge => ge.Authors)
// .ThenBy(ge => ge.Series)
// .ThenBy(ge => ge.Title)
.ToList();
// BIND
gridEntryBindingSource.DataSource = new SortableBindingList<GridEntry>(orderedGridEntries);
// FILTER
Filter();
}
#endregion
#region Filter
private string _filterSearchString;
private void Filter() => Filter(_filterSearchString);
public void Filter(string searchString)
{
_filterSearchString = searchString;
if (_dataGridView.Rows.Count == 0)
return;
var searchResults = SearchEngineCommands.Search(searchString);
var productIds = searchResults.Docs.Select(d => d.ProductId).ToList();
// https://stackoverflow.com/a/18942430
var bindingContext = BindingContext[_dataGridView.DataSource];
bindingContext.SuspendBinding();
{
this.UIThreadSync(() =>
{
for (var r = _dataGridView.RowCount - 1; r >= 0; r--)
_dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
});
}
//Causes repainting of the DataGridView
bindingContext.ResumeBinding();
VisibleCountChanged?.Invoke(this, _dataGridView.AsEnumerable().Count(r => r.Visible));
}
#endregion
#region DataGridView Macro
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
#endregion
}
}

View File

@@ -1,336 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using AudibleApi.Authorization;
using DataLayer;
using Dinah.Core;
using InternalUtilities;
using LibationFileManager;
using LibationWinForms.Dialogs;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using Serilog;
namespace LibationWinForms
{
static class Program
{
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
static extern bool AllocConsole();
[STAThread]
static void Main()
{
try
{
//// Uncomment to see Console. Must be called before anything writes to Console.
//// Only use while debugging. Acts erratically in the wild
//AllocConsole();
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
//***********************************************//
// //
// do not use Configuration before this line //
// //
//***********************************************//
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations();
// do this as soon as possible (post-config)
RunInstaller(config);
// most migrations go in here
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
// migrations which require Forms or are long-running
RunWindowsOnlyMigrations(config);
MessageBoxVerboseLoggingWarning.ShowIfTrue();
#if !DEBUG
checkForUpdate();
#endif
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
}
catch (Exception ex)
{
var title = "Fatal error, pre-logging";
var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
try
{
MessageBoxAlertAdmin.Show(body, title, ex);
}
catch
{
MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return;
}
Application.Run(new Form1());
}
private static void RunInstaller(Configuration config)
{
// all returns should be preceded by either:
// - if config.LibationSettingsAreValid
// - error message, Exit()
if (config.LibationSettingsAreValid)
return;
var defaultLibationFilesDir = Configuration.UserProfile;
// check for existing settigns in default location
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
config.SetLibationFiles(defaultLibationFilesDir);
if (config.LibationSettingsAreValid)
return;
static void CancelInstallation()
{
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Application.Exit();
Environment.Exit(0);
}
var setupDialog = new SetupDialog();
if (setupDialog.ShowDialog() != DialogResult.OK)
{
CancelInstallation();
return;
}
if (setupDialog.IsNewUser)
config.SetLibationFiles(defaultLibationFilesDir);
else if (setupDialog.IsReturningUser)
{
var libationFilesDialog = new LibationFilesDialog();
if (libationFilesDialog.ShowDialog() != DialogResult.OK)
{
CancelInstallation();
return;
}
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
if (config.LibationSettingsAreValid)
return;
// path did not result in valid settings
var continueResult = MessageBox.Show(
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
"New install?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (continueResult != DialogResult.Yes)
{
CancelInstallation();
return;
}
}
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
config.Books ??= Path.Combine(defaultLibationFilesDir, "Books");
config.InProgress ??= Configuration.WinTemp;
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
{
CancelInstallation();
return;
}
if (config.LibationSettingsAreValid)
return;
CancelInstallation();
}
/// <summary>migrations which require Forms or are long-running</summary>
private static void RunWindowsOnlyMigrations(Configuration config)
{
// only supported in winforms. don't move to app scaffolding
migrate_to_v5_0_0(config);
// long running. won't get a chance to finish in cli. don't move to app scaffolding
migrate_to_v5_5_0(config);
}
#region migrate to v5.0.0 re-register device if device info not in settings
private static void migrate_to_v5_0_0(Configuration config)
{
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
return;
var accountsPersister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = accountsPersister?.AccountsSettings?.Accounts;
if (accounts is null)
return;
foreach (var account in accounts)
{
var identity = account?.IdentityTokens;
if (identity is null)
continue;
if (!string.IsNullOrWhiteSpace(identity.DeviceType) &&
!string.IsNullOrWhiteSpace(identity.DeviceSerialNumber) &&
!string.IsNullOrWhiteSpace(identity.AmazonAccountId))
continue;
var authorize = new Authorize(identity.Locale);
try
{
authorize.DeregisterAsync(identity.ExistingAccessToken, identity.Cookies.ToKeyValuePair()).GetAwaiter().GetResult();
identity.Invalidate();
// re-registers device
ApiExtended.CreateAsync(account, new Login.WinformLoginChoiceEager(account)).GetAwaiter().GetResult();
}
catch
{
// Don't care if it fails
}
}
}
#endregion
#region migrate to v5.5.0. FilePaths.json => db. long running. fire and forget
private static void migrate_to_v5_5_0(Configuration config)
=> new System.Threading.Thread(() => migrate_to_v5_5_0_thread(config)) { IsBackground = true }.Start();
private static void migrate_to_v5_5_0_thread(Configuration config)
{
try
{
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
if (!File.Exists(filePaths))
return;
var fileLocations = Path.Combine(config.LibationFiles, "FileLocations.json");
if (!File.Exists(fileLocations))
File.Copy(filePaths, fileLocations);
// files to be deleted at the end
var libhackFilesToDelete = new List<string>();
// .libhack files => errors
var libhackFiles = Directory.EnumerateDirectories(config.Books, "*.libhack", SearchOption.AllDirectories);
using var context = ApplicationServices.DbContexts.GetContext();
context.Books.Load();
var jArr = JArray.Parse(File.ReadAllText(filePaths));
foreach (var jToken in jArr)
{
var asinToken = jToken["Id"];
var fileTypeToken = jToken["FileType"];
var pathToken = jToken["Path"];
if (asinToken is null || fileTypeToken is null || pathToken is null ||
asinToken.Type != JTokenType.String || fileTypeToken.Type != JTokenType.Integer || pathToken.Type != JTokenType.String)
continue;
var asin = asinToken.Value<string>();
var fileType = (FileType)fileTypeToken.Value<int>();
var path = pathToken.Value<string>();
if (fileType == FileType.Unknown || fileType == FileType.AAXC)
continue;
var book = context.Books.Local.FirstOrDefault(b => b.AudibleProductId == asin);
if (book is null)
continue;
// assign these strings and enums/ints unconditionally. EFCore will only update if changed
if (fileType == FileType.PDF)
book.UserDefinedItem.BatchMode_UpdatePdfStatus(LiberatedStatus.Liberated);
if (fileType == FileType.Audio)
{
var lhack = libhackFiles.FirstOrDefault(f => f.ContainsInsensitive(asin));
if (lhack is null)
book.UserDefinedItem.BatchMode_UpdateBookStatus(LiberatedStatus.Liberated);
else
{
book.UserDefinedItem.BatchMode_UpdateBookStatus(LiberatedStatus.Error);
libhackFilesToDelete.Add(lhack);
}
}
}
// in order: save to db, full reindex from db, refresh ui
var changed = context.SaveChanges();
if (changed > 0)
ApplicationServices.SearchEngineCommands.FullReIndex();
UserDefinedItem.BatchMode_Finalize();
// only do this after save changes
foreach (var libhackFile in libhackFilesToDelete)
File.Delete(libhackFile);
File.Delete(filePaths);
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
}
}
#endregion
private static void checkForUpdate()
{
string zipUrl;
string htmlUrl;
string zipName;
try
{
bool hasUpgrade;
(hasUpgrade, zipUrl, htmlUrl, zipName) = AppScaffolding.LibationScaffolding.GetLatestRelease();
if (!hasUpgrade)
return;
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show("Error checking for update", "Error checking for update", ex);
return;
}
if (zipUrl is null)
{
MessageBox.Show(htmlUrl, "New version available");
return;
}
var result = MessageBox.Show($"New version available @ {htmlUrl}\r\nDownload the zip file?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result != DialogResult.Yes)
return;
try
{
using var fileSelector = new SaveFileDialog { FileName = zipName, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
if (fileSelector.ShowDialog() != DialogResult.OK)
return;
var selectedPath = fileSelector.FileName;
BookLiberation.ProcessorAutomationController.DownloadFile(zipUrl, selectedPath, true);
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show("Error downloading update", "Error downloading update", ex);
}
}
}
}

277
README.md
View File

@@ -7,27 +7,35 @@
# Table of Contents
1. [Audible audiobook manager](#audible-audiobook-manager)
- [Audible audiobook manager](#audible-audiobook-manager)
- [The good](#the-good)
- [The bad](#the-bad)
- [The ugly](#the-ugly)
2. [Getting started](#getting-started)
- [Create Accounts](#create-accounts)
- [Import your library](#import-your-library)
- [Download your books -- DRM-free!](#download-your-books----drm-free)
- [Download PDF attachments](#download-pdf-attachments)
- [Details of downloaded files](#details-of-downloaded-files)
- [Export your library](#export-your-library)
3. [Searching and filtering](#searching-and-filtering)
- [Tags](#tags)
- [Searches](#searches)
- [Search examples](#search-examples)
- [Filters](#filters)
4. [Advanced](#advanced)
- [Files and folders](#files-and-folders)
- [Linux and Mac (unofficial)](#linux-and-mac)
- [Settings](#settings)
- [Command Line Interface](#command-line-interface)
- [Getting started](Documentation/GettingStarted.md)
- [Download Libation](Documentation/GettingStarted.md#download-libation-1)
- [Installation](Documentation/GettingStarted.md#installation)
- [Create Accounts](Documentation/GettingStarted.md#create-accounts)
- [Import your library](Documentation/GettingStarted.md#import-your-library)
- [Download your books -- DRM-free!](Documentation/GettingStarted.md#download-your-books----drm-free)
- [Download PDF attachments](Documentation/GettingStarted.md#download-pdf-attachments)
- [Details of downloaded files](Documentation/GettingStarted.md#details-of-downloaded-files)
- [Export your library](Documentation/GettingStarted.md#export-your-library)
- [Searching and filtering](Documentation/SearchingAndFiltering.md)
- [Tags](Documentation/SearchingAndFiltering.md#tags)
- [Searches](Documentation/SearchingAndFiltering.md#searches)
- [Search examples](Documentation/SearchingAndFiltering.md#search-examples)
- [Filters](Documentation/SearchingAndFiltering.md#filters)
- [Advanced](Documentation/Advanced.md)
- [Files and folders](Documentation/Advanced.md#files-and-folders)
- [Linux and Mac (unofficial)](Documentation/Advanced.md#linux-and-mac)
- [Settings](Documentation/Advanced.md#settings)
- [Custom File Naming](Documentation/Advanced.md#custom-file-naming)
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
## Getting started
* [Download](https://github.com/rmcrackan/Libation/releases/latest)
* [Step-by-step walk-through](Documentation/GettingStarted.md)
## Audible audiobook manager
@@ -47,7 +55,6 @@
### The bad
* Windows only
* Several known speed/performance issues
* Large file size
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows
@@ -60,235 +67,3 @@
Disclaimer: I've made every good-faith effort to include nothing insecure, malicious, anti-privacy, or destructive. That said: use at your own risk.
I made this for myself and I want to share it with the great programming and audible/audiobook communiites which have been so generous with their time and help.
## Getting started
#### [Download Libation](https://github.com/rmcrackan/Libation/releases)
### Create Accounts
Create your account(s):
![Create your accounts, menu](images/v40_accounts.png)
New locale options include many more regions including old audible accounts which pre-date the amazon acquisition
![Choose your account locales](images/v40_locales.png)
### Import your library
Select Import > Scan Library:
![Import step 1](images/Import1.png)
Or if you have multiple accounts, you'll get to choose whether to scan all accounts or just the ones you select:
![Import which accounts](images/v40_import.png)
You'll see this window while it's scanning:
![Import step 2](images/Import2.png)
Success! We see how many new titles are imported:
![Import step 3](images/Import3.png)
### Download your books -- DRM-free!
Automatically download some or all of your audible books. This shows you how much of your library is not yet downloaded and decrypted:
The stoplights will tell you a title's status:
* Green: downloaded and decrypted
* Yellow: downloaded but still encrypted with DRM
* Red: not downloaded
* PDF icon without arrow: downloaded
* PDF with arrow: not downloaded
Or hover over the button to see the status.
![Liberate book step 1](images/LiberateBook1.png)
Select Liberate > Begin Book Backups
You can also click on the stop light to download only that title and its PDF
![Liberate book step 2](images/LiberateBook2.png)
First the original book with DRM is downloaded
![Liberate book step 3](images/LiberateBook3.png)
Then it's decrypted so you can use it on any device you choose. The very first time you decrypt a book, this step will take a while. Every other book will go much faster. The first time, Libation has to figure out the special decryption key which allows your personal books to be unlocked.
![Liberate book step 4](images/LiberateBook4.png)
And voila! If you have multiple books not yet liberated, Libation will automatically move on to the next.
![Liberate book step 5](images/LiberateBook5.png)
The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
### Download PDF attachments
For books which include PDF downloads, Libation can download these for you as well and will attempt to store them with the book. "Book backup" will already download an available PDF. This additional option is useful when Audible adds a PDF to your book after you've already backed it up.
Select Liberate > Begin PDF Backups
![PDF download step 2](images/PdfDownload2.png)
The downloads work just like with books, only with no additional decryption needed.
![PDF download step 3](images/PdfDownload3.png)
### Details of downloaded files
![Post download](images/PostDownload.png)
When you set up Libation, you'll specify a Books directory. Libation looks inside that directory and all subdirectories to look for files or folders with each library book's audible id. This way, organization is completely up to you. When you download + decrypt a book, you get several files
* .m4b: your audiobook in m4b format. This is the most pure version of your audiobook and retains the highest quality. Now that it's decrypted, you can play it on any audio player and put it on any device. If you'd like, you can also use 3rd party tools to turn it into an mp3. The freedom to do what you want with your files was the original inspiration for Libation.
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
* .nfo: This is just some general info about the book and includes some technical stats about the audiofile.
### Export your library
![Export](images/Export.png)
Export your library to Excel, CSV, or JSON
## Searching and filtering
### Tags
To add tags to a title, click the tags button
![Tags step 1](images/Tags1.png)
Add as many tags as you'd like. Tags are separated by a space. Each tag can contain letters, numbers, and underscores
![Tags step 2](images/Tags2.png)
Tags are saved non-case specific for easy search. There is one special tag "hidden" which will also grey-out the book
![Tags step 3](images/Tags3.png)
To edit tags, just click the button again.
### Searches
Libation's advanced searching is built on the powerful Lucene search engine. Simple searches are effortless and powerful searches are simple. To search, just type and click Filter or press enter
* Type anything in the search box to search common fields: title, authors, narrators, and the book's audible id
* Use Lucene's "Query Parser Syntax" for advanced searching.
* Easy tutorial: http://www.lucenetutorial.com/lucene-query-syntax.html
* Full official guide: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html
* Tons of search fields, specific to audiobooks
* Synonyms so you don't have to memorize magic words. Eg: author and author**s** will both work
* Click [?] button for a full list of search fields and synonyms ![Filter options](images/FilterOptions.png)
* Search by tag like \[this\]
* When tags have an underscore you can use part of the tag. This is useful for quick categories. The below examples make this more clear.
### Search examples
Search for anything with the word potter
![Search example: potter](images/SearchExamplePotter.png)
If you only want to see Harry Potter
![Search example: "harry potter"](images/SearchExampleHarryPotter.png)
If you only want to see potter except for Harry Potter. You can also use "-" instead of "NOT"
![Search example: "potter NOT harry"](images/SearchExamplePotterNotHarry.png)
![Search example: "potter -harry"](images/SearchExamplePotterNotHarry2.png)
To see only books written by Neil Gaiman where he also narrates his own book. (If you don't include AND, you'll see everything written by Neil Gaiman and also all books in your library which are self-narrated.)
![Search example: author:gaiman AND authornarrated](images/SearchExampleGaimanAuthorNarrated.png)
I tagged autobiographies as auto_bio and biographies written by someone else as bio. I can get only autobiographies with \[auto_bio\] or get both by searching \[bio\]
![Search example: \[bio\]](images/SearchExampleBio.png)
![Search example: \[auto_bio\]](images/SearchExampleAutoBio.png)
### Filters
If you have a search you want to save, click Add To Quick Filters to save it in your Quick Filters list. To use it again, select it from the Quick Filters list.
To edit this list go to Quick Filters > Edit quick filters. Here you can re-order the list, delete filters, double-click a filter to edit it, or double-click the bottom blank box to add a new filter.
Check "Quick Filters > Start Libation with 1st filter Default" to have your top filter automatically applied when Libation starts. In this top example, I want to always start without these: at books I've tagged hidden, books I've tagged as free_audible_originals, and books which I have rated.
![default filters](images/FiltersDefault.png)
## Advanced
### Files and folders
To make upgrades and reinstalls easier, Libation separates all of its responsibilities to a few different folders. If you don't want to mess with this stuff: ignore it. Read on if you like a little more control over your files.
* In Libation's initial folder are the files that make up the program. Since nothing else is here, just copy new files here to upgrade the program. Delete this folder to delete Libation.
* In a separate folder, Libation keeps track of all of the files it creates like settings and downloaded images. After an upgrade, Libation might think that's its being run for the first time. Just click ADVANCED SETUP and point to this folder. Libation will reload your library and settings.
* The last important folder is the "books location." This is where Libation looks for your downloaded and decrypted books. This is how it knows which books still need to be downloaded. The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
### Linux and Mac
Although Libation only currently officially supports Windows, [some users](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158) have had success with WINE.
### Settings
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
### Command Line Interface
Libationcli.exe allows limited access to Libation's functionalities as a CLI.
Warnings about relying solely on on the CLI:
* CLI will not perform any upgrades.
* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
```
help
libationcli --help
verb-specific help
libationcli scan --help
scan all libraries
libationcli scan
scan only libraries for specific accounts
libationcli scan nickname1 nickname2
convert all m4b files to mp3
libationcli convert
liberate all books and pdfs
libationcli liberate
liberate pdfs only
libationcli liberate --pdf
libationcli liberate -p
export library to file
libationcli export --path "C:\foo\bar\my.json" --json
libationcli export -p "C:\foo\bar\my.json" -j
libationcli export -p "C:\foo\bar\my.csv" --csv
libationcli export -p "C:\foo\bar\my.csv" -c
libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
libationcli export -p "C:\foo\bar\my.xlsx" -x
```
Currently logs are written to Console and to file. This means they'll be printed in the CLI. To disable, find this in Settings.json and delete the 3 lines after `"WriteTo": [`
```
"Serilog": {
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "Console"
},
```

View File

@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean" Version="0.1.9" />
<PackageReference Include="Dinah.Core" Version="2.0.1.1" />
<PackageReference Include="AAXClean" Version="0.4.7" />
<PackageReference Include="AAXClean.Codecs" Version="0.2.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,33 +6,62 @@ namespace AaxDecrypter
{
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
{
protected OutputFormat OutputFormat { get; }
public event EventHandler<AppleTags> RetrievedMetadata;
protected AaxFile AaxFile;
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
: base(outFileName, cacheDirectory, dlLic)
{
OutputFormat = outputFormat;
}
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic) { }
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
public override void SetCoverArt(byte[] coverArt)
{
base.SetCoverArt(coverArt);
if (coverArt is not null)
AaxFile?.AppleTags.SetCoverArt(coverArt);
if (coverArt is not null && AaxFile?.AppleTags is not null)
AaxFile.AppleTags.Cover = coverArt;
}
protected bool Step_GetMetadata()
{
AaxFile = new AaxFile(InputFileStream);
if (DownloadOptions.StripUnabridged)
{
AaxFile.AppleTags.Title = AaxFile.AppleTags.TitleSansUnabridged;
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
}
//Finishing configuring lame encoder.
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
{
double bitrateMultiple = 1;
if (AaxFile.AudioChannels == 2)
{
if (DownloadOptions.Downsample)
bitrateMultiple = 0.5;
else
DownloadOptions.LameConfig.Mode = NAudio.Lame.MPEGMode.Stereo;
}
if (DownloadOptions.MatchSourceBitrate)
{
int kbps = (int)(AaxFile.AverageBitrate * bitrateMultiple / 1024);
if (DownloadOptions.LameConfig.VBR is null)
DownloadOptions.LameConfig.BitRate = kbps;
else if (DownloadOptions.LameConfig.VBR == NAudio.Lame.VBRMode.ABR)
DownloadOptions.LameConfig.ABRRateKbps = kbps;
}
}
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]");
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
RetrievedMetadata?.Invoke(this, AaxFile.AppleTags);
return !IsCanceled;
}
@@ -47,7 +76,7 @@ namespace AaxDecrypter
OnDecryptProgressUpdate(zeroProgress);
AaxFile.SetDecryptionKey(DownloadLicense.AudibleKey, DownloadLicense.AudibleIV);
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
return zeroProgress;
}
@@ -69,7 +98,7 @@ namespace AaxDecrypter
if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
var progressPercent = e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
var progressPercent = (e.ProcessPosition / e.TotalDuration);
OnDecryptProgressUpdate(
new DownloadProgress

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using AAXClean;
using AAXClean.Codecs;
using Dinah.Core.StepRunner;
using FileManager;
@@ -12,20 +13,24 @@ namespace AaxDecrypter
{
protected override StepSequence Steps { get; }
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private Func<MultiConvertFileProperties, string> multipartFileNameCallback { get; }
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private List<string> multiPartFilePaths { get; } = new List<string>();
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
: base(outFileName, cacheDirectory, dlLic, outputFormat)
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic,
Func<MultiConvertFileProperties, string> multipartFileNameCallback = null)
: base(outFileName, cacheDirectory, dlLic)
{
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + OutputFormat,
Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat,
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
["Step 3: Cleanup"] = Step_Cleanup,
};
this.multipartFileNameCallback = multipartFileNameCallback ?? MultiConvertFileProperties.DefaultMultipartFilename;
}
/*
@@ -56,10 +61,10 @@ That naming may not be desirable for everyone, but it's an easy change to instea
{
var zeroProgress = Step_DownloadAudiobook_Start();
var chapters = DownloadLicense.ChapterInfo.Chapters.ToList();
var chapters = DownloadOptions.ChapterInfo.Chapters.ToList();
// Ensure split files are at least minChapterLength in duration.
var splitChapters = new ChapterInfo();
var splitChapters = new ChapterInfo(DownloadOptions.ChapterInfo.StartOffset);
var runningTotal = TimeSpan.Zero;
string title = "";
@@ -81,50 +86,56 @@ That naming may not be desirable for everyone, but it's an easy change to instea
// reset, just in case
multiPartFilePaths.Clear();
ConversionResult result;
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
if (OutputFormat == OutputFormat.M4b)
ConvertToMultiMp4a(splitChapters);
if (DownloadOptions.OutputFormat == OutputFormat.M4b)
result = ConvertToMultiMp4a(splitChapters);
else
ConvertToMultiMp3(splitChapters);
result = ConvertToMultiMp3(splitChapters);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step_DownloadAudiobook_End(zeroProgress);
var success = !IsCanceled;
if (success)
foreach (var path in multiPartFilePaths)
OnFileCreated(path);
return success;
return result == ConversionResult.NoErrorsDetected;
}
private void ConvertToMultiMp4a(ChapterInfo splitChapters)
private ConversionResult ConvertToMultiMp4a(ChapterInfo splitChapters)
{
var chapterCount = 0;
AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback)
);
return AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback),
DownloadOptions.TrimOutputToChapterLength);
}
private void ConvertToMultiMp3(ChapterInfo splitChapters)
private ConversionResult ConvertToMultiMp3(ChapterInfo splitChapters)
{
var chapterCount = 0;
AaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
return AaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
{
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback);
newSplitCallback.LameConfig.ID3.Track = chapterCount.ToString();
});
((NAudio.Lame.LameConfig)newSplitCallback.UserState).ID3.Track = chapterCount.ToString();
}, DownloadOptions.LameConfig, DownloadOptions.TrimOutputToChapterLength);
}
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
{
var fileName = FileUtility.GetMultipartFileName(OutputFileName, currentChapter, splitChapters.Count, newSplitCallback.Chapter.Title);
var fileName = multipartFileNameCallback(new()
{
OutputFileName = OutputFileName,
PartsPosition = currentChapter,
PartsTotal = splitChapters.Count,
Title = newSplitCallback?.Chapter?.Title
});
fileName = FileUtility.GetValidFilename(fileName);
multiPartFilePaths.Add(fileName);
FileUtility.SaferDelete(fileName);
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
OnFileCreated(fileName);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using AAXClean;
using AAXClean.Codecs;
using Dinah.Core.StepRunner;
using FileManager;
@@ -10,12 +11,12 @@ namespace AaxDecrypter
{
protected override StepSequence Steps { get; }
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
: base(outFileName, cacheDirectory, dlLic, outputFormat)
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic)
{
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + OutputFormat,
Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat,
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsSingleFile,
@@ -31,15 +32,16 @@ namespace AaxDecrypter
FileUtility.SaferDelete(OutputFileName);
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OnFileCreated(OutputFileName);
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
var decryptionResult
= OutputFormat == OutputFormat.M4b
? AaxFile.ConvertToMp4a(outputFile, DownloadLicense.ChapterInfo)
: AaxFile.ConvertToMp3(outputFile);
= DownloadOptions.OutputFormat == OutputFormat.M4b
? AaxFile.ConvertToMp4a(outputFile, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength)
: AaxFile.ConvertToMp3(outputFile, DownloadOptions.LameConfig, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
DownloadLicense.ChapterInfo = AaxFile.Chapters;
DownloadOptions.ChapterInfo = AaxFile.Chapters;
Step_DownloadAudiobook_End(zeroProgress);

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dinah.Core;
using Dinah.Core.Net.Http;
@@ -19,9 +20,10 @@ 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 DownloadLicense DownloadLicense { get; }
protected DownloadOptions DownloadOptions { get; }
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
// Don't give the property a 'set'. This should have to be an obvious choice; not accidental
@@ -30,11 +32,10 @@ namespace AaxDecrypter
protected abstract StepSequence Steps { get; }
private NetworkFileStreamPersister nfsPersister;
private string cacheDir { get; }
private string jsonDownloadState => Path.Combine(cacheDir, Path.ChangeExtension(OutputFileName, ".json"));
private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp");
private string jsonDownloadState { get; }
public string TempFilePath { get; }
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
{
OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
@@ -44,11 +45,14 @@ namespace AaxDecrypter
if (!Directory.Exists(cacheDirectory))
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}");
cacheDir = cacheDirectory;
jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json")));
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
DownloadOptions = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
// delete file after validation is complete
FileUtility.SaferDelete(OutputFileName);
DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
}
public abstract void Cancel();
@@ -67,16 +71,18 @@ namespace AaxDecrypter
Serilog.Log.Logger.Error("Conversion failed");
return IsSuccess;
}
}
protected void OnRetrievedTitle(string title)
protected void OnRetrievedTitle(string title)
=> RetrievedTitle?.Invoke(this, title);
protected void OnRetrievedAuthors(string authors)
protected void OnRetrievedAuthors(string authors)
=> RetrievedAuthors?.Invoke(this, authors);
protected void OnRetrievedNarrators(string narrators)
protected void OnRetrievedNarrators(string narrators)
=> RetrievedNarrators?.Invoke(this, narrators);
protected void OnRetrievedCoverArt(byte[] coverArt)
protected void OnRetrievedCoverArt(byte[] coverArt)
=> RetrievedCoverArt?.Invoke(this, coverArt);
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
@@ -92,12 +98,14 @@ namespace AaxDecrypter
protected bool Step_CreateCue()
{
if (!DownloadOptions.CreateCueSheet) return true;
// not a critical step. its failure should not prevent future steps from running
try
{
var path = Path.ChangeExtension(OutputFileName, ".cue");
path = FileUtility.GetValidFilename(path);
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
OnFileCreated(path);
}
catch (Exception ex)
@@ -109,9 +117,31 @@ namespace AaxDecrypter
protected bool Step_Cleanup()
{
FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(tempFile);
return !IsCanceled;
bool success = !IsCanceled;
if (success)
{
FileUtility.SaferDelete(jsonDownloadState);
if (DownloadOptions.AudibleKey is not null &&
DownloadOptions.AudibleIV is not null &&
DownloadOptions.RetainEncryptedFile)
{
string aaxPath = Path.ChangeExtension(TempFilePath, ".aax");
FileUtility.SaferMove(TempFilePath, aaxPath);
//Write aax decryption key
string keyPath = Path.ChangeExtension(aaxPath, ".key");
FileUtility.SaferDelete(keyPath);
File.WriteAllText(keyPath, $"Key={DownloadOptions.AudibleKey}\r\nIV={DownloadOptions.AudibleIV}");
OnFileCreated(aaxPath);
OnFileCreated(keyPath);
}
else
FileUtility.SaferDelete(TempFilePath);
}
return success;
}
private NetworkFileStreamPersister OpenNetworkFileStream()
@@ -124,13 +154,13 @@ namespace AaxDecrypter
var nfsp = new NetworkFileStreamPersister(jsonDownloadState);
// If More than ~1 hour has elapsed since getting the download url, it will expire.
// The new url will be to the same file.
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadLicense.DownloadUrl));
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl));
return nfsp;
}
catch
{
FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(tempFile);
FileUtility.SaferDelete(TempFilePath);
return NewNetworkFilePersister();
}
}
@@ -139,10 +169,10 @@ namespace AaxDecrypter
{
var headers = new System.Net.WebHeaderCollection
{
{ "User-Agent", DownloadLicense.UserAgent }
{ "User-Agent", DownloadOptions.UserAgent }
};
var networkFileStream = new NetworkFileStream(tempFile, new Uri(DownloadLicense.DownloadUrl), 0, headers);
var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, headers);
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
}

View File

@@ -14,14 +14,17 @@ namespace AaxDecrypter
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
var startOffset = chapters.StartOffset;
var trackCount = 0;
foreach (var c in chapters.Chapters)
{
var startTime = c.StartOffset - startOffset;
trackCount++;
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)c.StartOffset.TotalMinutes}:{c.StartOffset:ss\\:ff}");
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds / 1000d * 75)}");
}
return stringBuilder.ToString();

View File

@@ -0,0 +1,30 @@
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public class DownloadOptions
{
public string DownloadUrl { get; }
public string UserAgent { get; }
public string AudibleKey { get; init; }
public string AudibleIV { get; init; }
public OutputFormat OutputFormat { get; init; }
public bool TrimOutputToChapterLength { get; init; }
public bool RetainEncryptedFile { get; init; }
public bool StripUnabridged { get; init; }
public bool CreateCueSheet { get; init; }
public ChapterInfo ChapterInfo { get; set; }
public NAudio.Lame.LameConfig LameConfig { get; set; }
public bool Downsample { get; set; }
public bool MatchSourceBitrate { get; set; }
public DownloadOptions(string downloadUrl, string userAgent)
{
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check for key/iv. unencrypted files do not have them
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.IO;
using FileManager;
namespace AaxDecrypter
{
public class MultiConvertFileProperties
{
public string OutputFileName { get; set; }
public int PartsPosition { get; set; }
public int PartsTotal { get; set; }
public string Title { get; set; }
public static string DefaultMultipartFilename(MultiConvertFileProperties multiConvertFileProperties)
{
var template = Path.ChangeExtension(multiConvertFileProperties.OutputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(multiConvertFileProperties.OutputFileName);
var fileNamingTemplate = new FileNamingTemplate(template) { IllegalCharacterReplacements = " " };
fileNamingTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(multiConvertFileProperties.PartsPosition, multiConvertFileProperties.PartsTotal));
fileNamingTemplate.AddParameterReplacement("title", multiConvertFileProperties.Title ?? "");
return fileNamingTemplate.GetFilePath();
}
}
}

View File

@@ -0,0 +1,443 @@
using Dinah.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using System.Net;
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 = 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];
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
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => ContentLength + offset,
_ => offset,
};
WaitToPosition(newPosition);
return _readFile.Position = newPosition;
}
/// <summary>
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
{
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
}
public override void Close()
{
IsCancelled = true;
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
_readFile.Close();
_writeFile.Close();
_networkStream?.Close();
Update();
}
#endregion
~NetworkFileStream()
{
downloadEnded?.Close();
downloadedPiece?.Close();
}
}
}

View File

@@ -10,7 +10,7 @@ namespace AaxDecrypter
{
protected override StepSequence Steps { get; }
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadLicense dlLic)
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic)
{
Steps = new StepSequence

View File

@@ -2,8 +2,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Version>6.2.7.1</Version>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>7.7.2.1</Version>
</PropertyGroup>
<ItemGroup>
@@ -11,11 +11,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Octokit" Version="0.50.0" />
<PackageReference Include="Octokit" Version="0.51.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.IO;
using Dinah.Core.Logging;
using InternalUtilities;
using LibationFileManager;
using Newtonsoft.Json.Linq;
using Serilog;
@@ -50,15 +50,93 @@ namespace AppScaffolding
public static void RunPostConfigMigrations(Configuration config)
{
AudibleApiStorage.EnsureAccountsSettingsFileExists();
PopulateMissingConfigValues(config);
//
// migrations go below here
//
Migrations.migrate_to_v5_2_0__post_config(config);
Migrations.migrate_to_v5_7_1(config);
Migrations.migrate_to_v6_1_2(config);
Migrations.migrate_to_v6_2_0(config);
Migrations.migrate_to_v6_6_9(config);
}
public static void PopulateMissingConfigValues(Configuration config)
{
config.InProgress ??= Configuration.WinTemp;
if (!config.Exists(nameof(config.AllowLibationFixup)))
config.AllowLibationFixup = true;
if (!config.Exists(nameof(config.CreateCueSheet)))
config.CreateCueSheet = true;
if (!config.Exists(nameof(config.RetainAaxFile)))
config.RetainAaxFile = false;
if (!config.Exists(nameof(config.SplitFilesByChapter)))
config.SplitFilesByChapter = false;
if (!config.Exists(nameof(config.StripUnabridged)))
config.StripUnabridged = false;
if (!config.Exists(nameof(config.StripAudibleBrandAudio)))
config.StripAudibleBrandAudio = false;
if (!config.Exists(nameof(config.DecryptToLossy)))
config.DecryptToLossy = false;
if (!config.Exists(nameof(config.LameTargetBitrate)))
config.LameTargetBitrate = false;
if (!config.Exists(nameof(config.LameDownsampleMono)))
config.LameDownsampleMono = true;
if (!config.Exists(nameof(config.LameBitrate)))
config.LameBitrate = 64;
if (!config.Exists(nameof(config.LameConstantBitrate)))
config.LameConstantBitrate = false;
if (!config.Exists(nameof(config.LameMatchSourceBR)))
config.LameMatchSourceBR = true;
if (!config.Exists(nameof(config.LameVBRQuality)))
config.LameVBRQuality = 2;
if (!config.Exists(nameof(config.BadBook)))
config.BadBook = Configuration.BadBookAction.Ask;
if (!config.Exists(nameof(config.ShowImportedStats)))
config.ShowImportedStats = true;
if (!config.Exists(nameof(config.ImportEpisodes)))
config.ImportEpisodes = true;
if (!config.Exists(nameof(config.DownloadEpisodes)))
config.DownloadEpisodes = true;
if (!config.Exists(nameof(config.FolderTemplate)))
config.FolderTemplate = Templates.Folder.DefaultTemplate;
if (!config.Exists(nameof(config.FileTemplate)))
config.FileTemplate = Templates.File.DefaultTemplate;
if (!config.Exists(nameof(config.ChapterFileTemplate)))
config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
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;
}
/// <summary>Initialize logging. Run after migration</summary>
@@ -71,32 +149,15 @@ namespace AppScaffolding
private static void ensureSerilogConfig(Configuration config)
{
if (config.GetObject("Serilog") != null)
if (config.GetObject("Serilog") is not null)
return;
// "Serilog": {
// "MinimumLevel": "Information"
// "WriteTo": [
// {
// "Name": "Console"
// },
// {
// "Name": "File",
// "Args": {
// "rollingInterval": "Day",
// "outputTemplate": ...
// }
// }
// ],
// "Using": [ "Dinah.Core" ],
// "Enrich": [ "WithCaller" ]
// }
var serilogObj = new JObject
{
{ "MinimumLevel", "Information" },
{ "WriteTo", new JArray
{
new JObject { {"Name", "Console" } },
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
new JObject
{
{ "Name", "File" },
@@ -111,14 +172,16 @@ namespace AppScaffolding
// output example: 2019-11-26 08:48:40.224 -05:00 [DBG] Begin Libation
// - with class and method info: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}";
// output example: 2019-11-26 08:48:40.224 -05:00 [DBG] (at LibationWinForms.Program.init()) Begin Libation
{ "outputTemplate", "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}" }
// {Properties:j} needed for expanded exception logging
{ "outputTemplate", "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception} {Properties:j}" }
}
}
}
}
},
{ "Using", new JArray{ "Dinah.Core" } }, // dll's name, NOT namespace
{ "Enrich", new JArray{ "WithCaller" } },
// better exception logging with: Serilog.Exceptions library -- WithExceptionDetails
{ "Using", new JArray{ "Dinah.Core", "Serilog.Exceptions" } }, // dll's name, NOT namespace
{ "Enrich", new JArray{ "WithCaller", "WithExceptionDetails" } },
};
config.SetObject("Serilog", serilogObj);
}
@@ -133,9 +196,9 @@ namespace AppScaffolding
// capture most Console.WriteLine() and write to serilog. See below tests for details.
// Some dependencies print helpful info via Console.WriteLine. We'd like to log it.
//
// Serilog also writes to Console so this might be asking for trouble. ie: infinite loops.
// SerilogTextWriter needs to be more robust and tested. Esp the Write() methods.
// Empirical testing so far has shown no issues.
// If Serilog also writes to Console, this might be asking for trouble. ie: infinite loops.
// To use that way, SerilogTextWriter needs to be more robust and tested. Esp the Write() methods.
// However, empirical testing so far has shown no issues.
Console.SetOut(new MultiTextWriter(origOut, new SerilogTextWriter()));
#region Console => Serilog tests
@@ -211,30 +274,28 @@ namespace AppScaffolding
config.InProgress,
DownloadsInProgressDirectory = AudibleFileStorage.DownloadsInProgressDirectory,
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
AudibleFileStorage.DownloadsInProgressDirectory,
DownloadsInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
DecryptInProgressDirectory = AudibleFileStorage.DecryptInProgressDirectory,
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
AudibleFileStorage.DecryptInProgressDirectory,
DecryptInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
});
}
public static (bool hasUpgrade, string zipUrl, string htmlUrl, string zipName) GetLatestRelease()
public static UpgradeProperties GetLatestRelease()
{
(bool, string, string, string) isFalse = (false, null, null, null);
// 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"));
@@ -247,7 +308,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)
{
@@ -311,39 +372,53 @@ namespace AppScaffolding
"WinTemp" => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")),
_ => path
};
public static void migrate_to_v5_2_0__post_config(Configuration config)
{
if (!config.Exists(nameof(config.AllowLibationFixup)))
config.AllowLibationFixup = true;
if (!config.Exists(nameof(config.DecryptToLossy)))
config.DecryptToLossy = false;
}
#endregion
// add config.BadBook
public static void migrate_to_v5_7_1(Configuration config)
public static void migrate_to_v6_6_9(Configuration config)
{
if (!config.Exists(nameof(config.BadBook)))
config.BadBook = Configuration.BadBookAction.Ask;
}
var writeToPath = $"Serilog.WriteTo";
// add config.DownloadEpisodes , config.ImportEpisodes
public static void migrate_to_v6_1_2(Configuration config)
{
if (!config.Exists(nameof(config.DownloadEpisodes)))
config.DownloadEpisodes = true;
// remove WriteTo[].Name == Console
{
if (UNSAFE_MigrationHelper.Settings_TryGetArrayLength(writeToPath, out var length1))
{
for (var i = length1 - 1; i >= 0; i--)
{
var exists = UNSAFE_MigrationHelper.Settings_TryGetFromJsonPath($"{writeToPath}[{i}].Name", out var value);
if (!config.Exists(nameof(config.ImportEpisodes)))
config.ImportEpisodes = true;
}
if (exists && value == "Console")
UNSAFE_MigrationHelper.Settings_RemoveFromArray(writeToPath, i);
}
}
}
// add config.SplitFilesByChapter
public static void migrate_to_v6_2_0(Configuration config)
{
if (!config.Exists(nameof(config.SplitFilesByChapter)))
config.SplitFilesByChapter = false;
// add Serilog.Exceptions -- WithExceptionDetails
{
// outputTemplate should contain "{Properties:j}"
{
// re-calculate. previous loop may have changed the length
if (UNSAFE_MigrationHelper.Settings_TryGetArrayLength(writeToPath, out var length2))
{
var propertyName = "outputTemplate";
for (var i = 0; i < length2; i++)
{
var jsonPath = $"{writeToPath}[{i}].Args";
var exists = UNSAFE_MigrationHelper.Settings_TryGetFromJsonPath($"{jsonPath}.{propertyName}", out var value);
var newValue = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception} {Properties:j}";
if (exists && value != newValue)
UNSAFE_MigrationHelper.Settings_SetWithJsonPath(jsonPath, propertyName, newValue);
}
}
}
// Serilog.Using must include "Serilog.Exceptions"
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Using", "Serilog.Exceptions");
// Serilog.Enrich must include "WithExceptionDetails"
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
}
}
}
}

View File

@@ -113,6 +113,108 @@ namespace AppScaffolding
return success;
}
public static bool Settings_JsonPathIsType(string jsonPath, JTokenType jTokenType)
{
JToken val = null;
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
return val?.Type == jTokenType;
}
public static bool Settings_TryGetFromJsonPath(string jsonPath, out string value)
{
JToken val = null;
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
if (val?.Type == JTokenType.String)
{
value = val.Value<string>();
return true;
}
else
{
value = null;
return false;
}
}
public static void Settings_SetWithJsonPath(string jsonPath, string propertyName, string newValue)
{
if (!Settings_TryGetFromJsonPath($"{jsonPath}.{propertyName}", out _))
return;
process_SettingsJson(jObj =>
{
var token = jObj.SelectToken(jsonPath);
if (token is null
|| token is not JObject o
|| o[propertyName] is null)
return;
var oldValue = token.Value<string>(propertyName);
if (oldValue != newValue)
token[propertyName] = newValue;
});
}
public static bool Settings_TryGetArrayLength(string jsonPath, out int length)
{
length = 0;
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return false;
JArray array = null;
process_SettingsJson(jObj => array = (JArray)jObj.SelectToken(jsonPath));
length = array.Count;
return true;
}
public static void Settings_AddToArray(string jsonPath, string newValue)
{
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return;
process_SettingsJson(jObj =>
{
var array = (JArray)jObj.SelectToken(jsonPath);
array.Add(newValue);
});
}
/// <summary>Do not add if already exists</summary>
public static void Settings_AddUniqueToArray(string arrayPath, string newValue)
{
if (!Settings_TryGetArrayLength(arrayPath, out var qty))
return;
for (var i = 0; i < qty; i++)
{
var exists = Settings_TryGetFromJsonPath($"{arrayPath}[{i}]", out var value);
if (exists && value == newValue)
return;
}
Settings_AddToArray(arrayPath, newValue);
}
/// <summary>only remove if not exists</summary>
public static void Settings_RemoveFromArray(string jsonPath, int position)
{
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return;
process_SettingsJson(jObj =>
{
var array = (JArray)jObj.SelectToken(jsonPath);
if (position < array.Count)
array.RemoveAt(position);
});
}
/// <summary>only insert if not exists</summary>
public static void Settings_Insert(string key, string value)
=> process_SettingsJson(jObj => jObj.TryAdd(key, value));

View File

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

View File

@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.1.1" />
<PackageReference Include="NPOI" Version="2.5.4" />
<PackageReference Include="CsvHelper" Version="27.2.1" />
<PackageReference Include="NPOI" Version="2.5.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleUtilities;
using DataLayer;
using Dinah.Core;
using DtoImporterService;
using InternalUtilities;
using LibationFileManager;
using Serilog;
using static DtoImporterService.PerfLogger;
@@ -15,24 +15,38 @@ namespace ApplicationServices
{
public static class LibraryCommands
{
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, List<LibraryBook> existingLibrary, params Account[] accounts)
public static event EventHandler<int> ScanBegin;
public static event EventHandler ScanEnd;
public static bool Scanning { get; private set; }
private static object _lock { get; } = new();
static LibraryCommands()
{
ScanBegin += (_, __) => Scanning = true;
ScanEnd += (_, __) => Scanning = false;
}
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, List<LibraryBook> existingLibrary, params Account[] accounts)
{
logRestart();
//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;
@@ -82,13 +96,28 @@ namespace ApplicationServices
try
{
lock (_lock)
{
if (Scanning)
return (0, 0);
ScanBegin?.Invoke(null, accounts.Length);
}
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;
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
if (totalCount == 0)
return default;
Log.Logger.Information("Begin long-running import");
logTime($"pre {nameof(importIntoDbAsync)}");
var newCount = await importIntoDbAsync(importItems);
@@ -124,10 +153,11 @@ namespace ApplicationServices
{
stop();
var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null);
}
}
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)
@@ -136,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
@@ -145,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));
@@ -156,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}");
@@ -164,25 +194,47 @@ namespace ApplicationServices
}
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
{
logTime("importIntoDbAsync -- pre db");
using var context = DbContexts.GetContext();
var libraryBookImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()");
int qtyChanges = saveChanges(context);
logTime("importIntoDbAsync -- post SaveChanges");
// this is any changes at all to the database, not just new books
if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange());
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
return newCount;
}
private static int saveChanges(LibationContext context)
{
logTime("importIntoDbAsync -- pre db");
using var context = DbContexts.GetContext();
var libraryImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()");
var qtyChanges = context.SaveChanges();
logTime("importIntoDbAsync -- post SaveChanges");
try
{
return context.SaveChanges();
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
{
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culpret is the "WithExceptionDetails" serilog extension
if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange());
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}";
return newCount;
}
#endregion
var msg = "Microsoft.EntityFrameworkCore.DbUpdateException";
if (ex.InnerException is null)
throw new Exception($"{msg}{format(ex)}");
throw new Exception(
$"{msg}{format(ex)}",
new Exception($"Inner Exception{format(ex.InnerException)}"));
}
}
#endregion
#region remove books
public static Task<List<LibraryBook>> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
#region remove books
public static Task<List<LibraryBook>> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
private static List<LibraryBook> removeBooks(List<string> idsToRemove)
{
using var context = DbContexts.GetContext();
@@ -207,37 +259,53 @@ namespace ApplicationServices
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 BookUserDefinedItemCommitted;
#region Update book details
public static int UpdateUserDefinedItem(Book book)
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)
if (qtyChanges == 0)
return 0;
// semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
// I did not benchmark before choosing the number here
if (qtyChanges > 15)
SearchEngineCommands.FullReIndex();
else
{
SearchEngineCommands.UpdateLiberatedStatus(book);
SearchEngineCommands.UpdateBookTags(book);
BookUserDefinedItemCommitted?.Invoke(null, book.AudibleProductId);
foreach (var book in books)
{
SearchEngineCommands.UpdateLiberatedStatus(book);
SearchEngineCommands.UpdateBookTags(book);
}
}
BookUserDefinedItemCommitted?.Invoke(null, null);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, $"Error updating {nameof(book.UserDefinedItem)}");
Log.Logger.Error(ex, $"Error updating {nameof(Book.UserDefinedItem)}");
throw;
}
}
@@ -245,7 +313,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;
@@ -272,7 +340,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

@@ -44,6 +44,9 @@ namespace ApplicationServices
[Name("Length In Minutes")]
public int LengthInMinutes { get; set; }
[Name("Description")]
public string Description { get; set; }
[Name("Publisher")]
public string Publisher { get; set; }
@@ -108,12 +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,
@@ -121,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,
@@ -180,6 +184,7 @@ namespace ApplicationServices
nameof (ExportDto.AuthorNames),
nameof (ExportDto.NarratorNames),
nameof (ExportDto.LengthInMinutes),
nameof (ExportDto.Description),
nameof (ExportDto.Publisher),
nameof (ExportDto.HasPdf),
nameof (ExportDto.SeriesNames),
@@ -233,6 +238,7 @@ namespace ApplicationServices
row.CreateCell(col++).SetCellValue(dto.AuthorNames);
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
row.CreateCell(col++).SetCellValue(dto.Description);
row.CreateCell(col++).SetCellValue(dto.Publisher);
row.CreateCell(col++).SetCellValue(dto.HasPdf);
row.CreateCell(col++).SetCellValue(dto.SeriesNames);

View File

@@ -6,7 +6,7 @@ using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
namespace InternalUtilities
namespace AudibleUtilities
{
public class Account : IUpdatable
{
@@ -73,10 +73,10 @@ namespace InternalUtilities
if (_identity is null && value is null)
return;
if (_identity != null)
if (_identity is not null)
_identity.Updated -= update;
if (value != null)
if (value is not null)
value.Updated += update;
_identity = value;

View File

@@ -6,7 +6,7 @@ using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
namespace InternalUtilities
namespace AudibleUtilities
{
// 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended
// from newtonsoft (https://www.newtonsoft.com/json/help/html/SerializationGuide.htm):
@@ -66,7 +66,7 @@ namespace InternalUtilities
{
var acct = GetAccount(accountId, locale);
if (acct != null)
if (acct is not null)
return acct;
var l = Localization.Get(locale);

View File

@@ -3,10 +3,16 @@ using AudibleApi.Authorization;
using Dinah.Core.IO;
using Newtonsoft.Json;
namespace InternalUtilities
namespace AudibleUtilities
{
public class AccountsSettingsPersister : JsonFilePersister<AccountsSettings>
{
public static event EventHandler Saving;
public static event EventHandler Saved;
protected override void OnSaving() => Saving?.Invoke(null, null);
protected override void OnSaved() => Saved?.Invoke(null, null);
/// <summary>Alias for Target </summary>
public AccountsSettings AccountsSettings => Target;

View File

@@ -8,7 +8,7 @@ using Dinah.Core;
using Polly;
using Polly.Retry;
namespace InternalUtilities
namespace AudibleUtilities
{
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public class ApiExtended
@@ -106,16 +106,16 @@ namespace InternalUtilities
// 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
@@ -127,11 +127,18 @@ namespace InternalUtilities
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
//}
#endif
Serilog.Log.Logger.Debug("Begin initial library scan");
if (!items.Any())
items = await Api.GetAllLibraryItemsAsync(responseGroups);
items = await Api.GetAllLibraryItemsAsync(libraryOptions);
Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan");
await manageEpisodesAsync(items, importEpisodes);
Serilog.Log.Logger.Debug("Episode scan complete");
#if DEBUG
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
#endif
@@ -141,7 +148,7 @@ namespace InternalUtilities
foreach (var v in validators)
{
var exceptions = v.Validate(items);
if (exceptions != null && exceptions.Any())
if (exceptions is not null && exceptions.Any())
throw new AggregateException(exceptions);
}
@@ -166,8 +173,7 @@ namespace InternalUtilities
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.
// also must happen before processing children because children abuses this flag
// remove episode parents. even if the following stuff fails, these will still be removed from the collection
items.RemoveAll(i => i.IsEpisodes);
if (importEpisodes)
@@ -192,6 +198,14 @@ namespace InternalUtilities
{
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())
{
results.Add(parent);
continue;
}
foreach (var child in children)
{
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
@@ -202,7 +216,8 @@ namespace InternalUtilities
new Series
{
Asin = parent.Asin,
Sequence = parent.Relationships.Single(r => r.Asin == child.Asin).Sort.ToString(),
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin).Sort.ToString(),
Title = parent.TitleWithSubtitle
}
};

View File

@@ -3,7 +3,7 @@ using System.IO;
using LibationFileManager;
using Newtonsoft.Json;
namespace InternalUtilities
namespace AudibleUtilities
{
public static class AudibleApiStorage
{

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
namespace InternalUtilities
namespace AudibleUtilities
{
public interface IValidator
{

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="2.3.1.1" />
<PackageReference Include="AudibleApi" Version="2.8.4.1" />
</ItemGroup>
<ItemGroup>

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