Compare commits

...

427 Commits

Author SHA1 Message Date
Robert McRackan
6254585ae2 New feature: auto-library updating 2022-05-09 15:28:00 -04: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
Robert McRackan
ab44823c05 Bug fix: update book series. Defensive FirstOrDefault 2021-10-18 20:40:56 -04:00
Robert McRackan
2767f04621 split AaxcDownload single and multi 2021-10-18 14:41:57 -04:00
Robert McRackan
0f1ff0aa10 minor refactor 2021-10-18 13:56:12 -04:00
Robert McRackan
c1af253300 fix partial rollback 2021-10-18 13:44:40 -04:00
Robert McRackan
d08962cffa Refactor valid path/filename. Centralize validaion. Universal templating is one step closer 2021-10-18 13:36:55 -04:00
Robert McRackan
7720110460 Bug fix: tag filters stopped working on 8/22 2021-10-15 14:45:35 -04:00
Robert McRackan
dfa5829cbd Safe(r)Delete, Safe(r)Move : could have infinite loop of exceptions. Fixed. Limit 3 2021-10-12 17:05:01 -04:00
Robert McRackan
648b84ee55 All audible-related file naming terminates at FileUtility
File extensions: Dinah.Core => Libation FileUtility
2021-10-12 14:48:32 -04:00
Robert McRackan
6a81b9b02d more LibationFileManager rename. and bug fix 2021-10-11 17:10:37 -04:00
Robert McRackan
c43e03b228 FileManager: separate generic from Libation-specific 2021-10-11 16:06:50 -04:00
Robert McRackan
1de7edd9df Chapter splitting: file names need more leading zeros when qty >100 2021-10-09 14:20:21 -04:00
Robert McRackan
df90094884 Replaced another id dependency with cache. Now safe for multi-file audiobooks. Also safe for current session not trying to move files created in a previous session or a parallel session of a different title 2021-10-08 21:34:42 -04:00
Robert McRackan
c9a6c8fd35 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-10-08 11:48:05 -04:00
Robert McRackan
d0b78cc501 New and moved files Upsert themselves in FileManager.FilePathCache 2021-10-08 11:47:54 -04:00
rmcrackan
0b7bc4d938 Update README.md
List all `libationcli export` options
2021-10-07 08:59:13 -04:00
Robert McRackan
18cca53968 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-10-07 08:45:19 -04:00
Robert McRackan
ef9c60cc4f File splitting: omit tiny chapters 2021-10-07 08:45:02 -04:00
rmcrackan
fa24831693 Merge pull request #135 from Mbucari/master
Fix chapter splitting.
2021-10-06 21:20:04 -04:00
Michael Bucari-Tovo
24370e9804 Merge branch 'master' of https://github.com/Mbucari/Libation 2021-10-06 16:02:30 -06:00
Michael Bucari-Tovo
d3f82b162e Fix chapter splitting. 2021-10-06 16:01:50 -06:00
rmcrackan
5a40c7370f Merge pull request #134 from Mbucari/master
Fix splitting audiobooks on chapters
2021-10-06 15:55:02 -04:00
Mbucari
2d22855b93 Merge branch 'rmcrackan:master' into master 2021-10-06 13:47:02 -06:00
Michael Bucari-Tovo
b870d562ff Only split chapters at least 15 seconds long. 2021-10-06 13:44:03 -06:00
Michael Bucari-Tovo
f1c87308ea Fixed access modifier. 2021-10-06 13:43:19 -06:00
Michael Bucari-Tovo
a3fac3441c Allow splitting book only if AllowLibationFixup is true. 2021-10-06 13:43:01 -06:00
Robert McRackan
5f8c672361 CLI: error when scan has new book with pdf attachment: 2021-10-06 15:35:19 -04:00
rmcrackan
40520b89d1 Merge pull request #132 from Mbucari/master
Convert IProcessable to abstract class Processable.
2021-10-06 11:06:25 -04:00
Michael Bucari-Tovo
0ac90f5a30 Discard unnused variable. 2021-10-06 08:31:37 -06:00
Michael Bucari-Tovo
4d6544d828 Revert accidental push of changes in progress. 2021-10-06 08:25:08 -06:00
Michael Bucari-Tovo
8098564926 Better naming. 2021-10-06 08:23:07 -06:00
Michael Bucari-Tovo
07c96c4994 Corrected access modifiers. 2021-10-06 08:22:50 -06:00
Michael Bucari-Tovo
aa8491f205 Edited comments 2021-10-05 16:54:33 -06:00
Michael Bucari-Tovo
5c535478d1 Add note 2021-10-05 16:49:55 -06:00
Michael Bucari-Tovo
f0541b498f Removed virtual 2021-10-05 16:49:06 -06:00
Michael Bucari-Tovo
e466d63e76 Convert IStreamable and IAudioDecodable to abstract classes. 2021-10-05 16:41:48 -06:00
Michael Bucari-Tovo
6e66314605 Convert IProcessable to abstract class Processable. 2021-10-05 16:10:56 -06:00
Robert McRackan
be5e18d977 settings descriptions 2021-10-05 11:09:36 -04:00
Robert McRackan
c437a39a82 New feature: download already split into chapters 2021-10-05 10:35:41 -04:00
Robert McRackan
7b55158148 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-10-05 10:08:21 -04:00
Robert McRackan
5772d9c31e defensive FirstOrDefault 2021-10-05 10:07:58 -04:00
rmcrackan
2a1f02b095 Merge pull request #127 from seanke/feature/multi_files
To mulitple files
2021-10-05 10:01:43 -04:00
Sean Kelly
5b7cde2a9e Fixed issues 2021-10-05 17:36:37 +13:00
Sean Kelly
5e349c6662 Removed repeated code 2021-09-30 20:32:30 +13:00
Sean Kelly
4b78b757aa Move files 2021-09-30 19:44:32 +13:00
Robert McRackan
22548dc8ae bug fix: if not importing episodes, remember to remove parents from import list 2021-09-29 10:00:04 -04:00
Robert McRackan
1165f81203 bug fix in series importer 2021-09-27 07:57:24 -04:00
Sean Kelly
13294d3414 Added m4b & mp3 methods for multiple files. 2021-09-27 21:34:43 +13:00
Sean Kelly
8a74a29700 Added configuration and wired it up. 2021-09-27 20:18:50 +13:00
Sean Kelly
36f58b64d6 proof of concept 2021-09-26 23:05:17 +13:00
Robert McRackan
19369a21ef * New feature: setting to not import episodes ( #125 ) 2021-09-25 14:02:27 -04:00
Robert McRackan
611fb4d6d8 increm ver 2021-09-24 20:11:52 -04:00
Robert McRackan
c77ec54035 bug fix: DownloadEpisodes logic needs parans 2021-09-24 19:59:57 -04:00
Robert McRackan
c9c28c7826 oops. again 2021-09-24 16:44:49 -04:00
Robert McRackan
30e2caaff5 New feature: setting to not download episodes 2021-09-24 16:44:28 -04:00
Robert McRackan
fd56017af5 dedicated lock objects for safety 2021-09-24 11:46:37 -04:00
Robert McRackan
d2eaf26117 Removing books
* message box is too big when removing huge amounts of books. This scenario is typical when removing podcasts
* removing books should remove user defined entries. eg: tags and is-liberated
2021-09-24 10:00:52 -04:00
Robert McRackan
7c38e18435 Add support for unencrypted mp3 audiobooks 2021-09-24 08:26:23 -04:00
Robert McRackan
bfb1dbc69a Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-09-24 08:25:16 -04:00
Robert McRackan
d2ff19e309 null description causes errors 2021-09-24 08:24:16 -04:00
rmcrackan
aa3a7dce06 Merge pull request #123 from Mbucari/master
Add support for unencrypted mp3 audiobooks.
2021-09-24 08:23:14 -04:00
Mbucari
71075838eb Moved event logging to LibationBaseForm 2021-09-23 19:29:25 -06:00
Michael Bucari-Tovo
803a0b7ccf Comment typo. 2021-09-23 18:14:29 -06:00
Michael Bucari-Tovo
d9f3fa825c Renaming and comments. 2021-09-23 18:13:43 -06:00
Michael Bucari-Tovo
df42ba584e Better naming. 2021-09-23 18:06:59 -06:00
Michael Bucari-Tovo
9f09a62a1e Ensure chapter info is downloaded for unencrypted MP3s 2021-09-23 18:04:12 -06:00
Michael Bucari-Tovo
e714179c30 Add support for unencrypted mp3 audiobooks. 2021-09-23 18:01:39 -06:00
Robert McRackan
db84c9a7d9 unencrypted podcast downloads (incomplete) 2021-09-23 16:50:59 -04:00
Robert McRackan
937bd56fcc debug code to incl episodes 2021-09-23 16:20:08 -04:00
Robert McRackan
f29968f379 conditional re-index 2021-09-23 14:44:11 -04:00
Robert McRackan
14e14ba9bd batch book status updates
bug fixes, esp. threading
2021-09-23 14:33:04 -04:00
Robert McRackan
613c97524a User get getting "bindingsource cannot be its own data source" error. Can't repro locally. Adding SyncBindingSource for datagrid views to attempt to solve it 2021-09-23 10:41:33 -04:00
Robert McRackan
4fd16f04e0 More robust error handling, especially before logging can be initialized 2021-09-23 09:19:35 -04:00
Robert McRackan
61385f0f0b more explicit book series order 2021-09-22 11:51:06 -04:00
Robert McRackan
7647882344 update audible api dependency 2021-09-22 09:48:28 -04:00
Robert McRackan
96ffa619ec Bug fix: podcast episodes with no title cause library import failure 2021-09-21 15:06:01 -04:00
Robert McRackan
de1147ac1b External login completed 2021-09-20 15:31:07 -04:00
Robert McRackan
926a7a1148 * include new external login interfaces in ApiExtended
* new ext method to simplify api access in file liberator
2021-09-17 16:09:49 -04:00
Robert McRackan
51020ef99e * upgrade api. lay the groundwork for supporting external logins. When complete, this will be v6.0
* replace AudibleApiActions with ApiExtended
2021-09-17 15:30:06 -04:00
Robert McRackan
5a1303c33a update dependency 2021-09-17 14:02:26 -04:00
Robert McRackan
a0e2d78b9b * upgraded AAXClean addresses issues #104 , #110
* update dependencies
2021-09-16 07:53:34 -04:00
rmcrackan
6b711190c3 Merge pull request #116 from Mbucari/master
Correctly write mp3 Xing header.
2021-09-16 07:46:03 -04:00
Michael Bucari-Tovo
b4a6342513 Revert accidental change. 2021-09-15 15:32:34 -06:00
Michael Bucari-Tovo
988b137d67 Updated AAXClean version. 2021-09-15 15:26:40 -06:00
Michael Bucari-Tovo
dae9c9c9b6 Add read permission to file stream. 2021-09-15 15:16:08 -06:00
Robert McRackan
420b7529c6 Bug fix: issues #113 , #114
Forcing a new build order. Seems to fix the issue. Since the issue was already intermittent (race condition?) it's difficult to be sure
2021-09-14 09:05:56 -04:00
Robert McRackan
4cf999c84d null book description throws 2021-09-13 11:05:44 -04:00
Robert McRackan
8fe3896d76 build error bug fix 2021-09-13 10:35:41 -04:00
Robert McRackan
adcba34560 'bad book' setting labels 2021-09-11 08:01:58 -04:00
Robert McRackan
8e09d7e617 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-09-10 22:59:02 -04:00
Robert McRackan
197b50e3ac issue #111 -- setting for bad book abort/retry/ignore. default: ask 2021-09-10 22:58:34 -04:00
rmcrackan
ac2114e270 Add CLI documentation 2021-09-10 17:20:13 -04:00
Robert McRackan
29461701cd I meant to increm the minor version for cli 2021-09-10 17:04:47 -04:00
Robert McRackan
0f130c70f5 LibationCli and structural changes to support it incl: no LibationLauncher, just LibationWinForms. LibationWinForms builds to a different output dir so cli can be deployed easily. Versioning number is moved to scaffolding library shared by both apps 2021-09-10 16:54:32 -04:00
Robert McRackan
995637e843 add logging. helpful when viewing in console 2021-09-10 12:51:07 -04:00
Robert McRackan
9501687f86 Change build path. Necessary for possibly building a non-winforms exe to the same output dir 2021-09-09 16:04:17 -04:00
Robert McRackan
248dea3402 write to log, not console 2021-09-09 13:26:00 -04:00
Robert McRackan
1d420f5430 File Liberators should log their own progresss and not depend on controller or forms to do so. This means that LogMe will create duplicate log entries for non-form and non-controller calls but that's a refactor for a rainy day. 2021-09-09 11:27:03 -04:00
Robert McRackan
5f0a6b8526 bug fix: installer bug for users who say 'return user' but don't have valid files 2021-09-07 16:58:04 -04:00
Robert McRackan
c337c0b44e library book composite key comments 2021-09-07 13:35:59 -04:00
Robert McRackan
89207866f3 make sure that __log is never empty so that .Last() can't throw 2021-09-04 18:17:29 -04:00
Robert McRackan
9e11086d49 bug fix 2021-09-04 18:09:51 -04:00
rmcrackan
58b172f816 Merge pull request #103 from Mbucari/master
Minor fix and changes for form size and location persistance.
2021-09-04 14:15:50 -04:00
Mbucari
0b8084bc03 Added form size and position persistance to audio decode forms. 2021-09-03 23:00:35 -06:00
Mbucari
37970222f3 Make SaveSizeAndLocation and RestoreSizeAndLocation a form extension. 2021-09-03 22:44:02 -06:00
Mbucari
bcab2dd440 Adjust display parameters. 2021-09-03 22:43:03 -06:00
Mbucari
d402128d1d GetNonString now handles values and classes. 2021-09-03 22:41:21 -06:00
Mbucari
3ae0f2daa2 Fixed FindInactiveBooks to work with new logger. 2021-09-03 22:40:35 -06:00
Robert McRackan
126919d578 update dependencies 2021-09-03 23:02:28 -04:00
Robert McRackan
437e85fd12 mp3 bugfix 2021-09-03 18:22:31 -04:00
Robert McRackan
de34e5c795 import speed improvements 2021-09-03 16:35:31 -04:00
Robert McRackan
8ffcefd6ae massive speed-up for import contributors 2021-09-03 15:30:17 -04:00
Robert McRackan
e59ab9b483 remove v3=>4 migration work-arounds 2021-09-03 13:42:58 -04:00
Robert McRackan
57fa1bd763 cross thread issue. add temp time logging in ImportAccountAsync 2021-09-03 11:36:55 -04:00
rmcrackan
dccb2d73d6 Merge pull request #101 from Mbucari/master
Fixed cross thread bug and Enumerable change bug.
2021-09-03 08:15:37 -04:00
Mbucari
77fc865636 Fixed crossthread. 2021-09-02 21:21:36 -06:00
Mbucari
1040a347c6 Fixed cross thread bug and Enumerable change bug. 2021-09-02 21:04:16 -06:00
Robert McRackan
6ed1307443 v5.6.3.1 : support for episodes ( issue #96 ) 2021-09-02 16:05:23 -04:00
Robert McRackan
c2c732b2b1 central event for library altered: books added or removed 2021-09-02 15:55:12 -04:00
Robert McRackan
9e0caf34d6 Rename db table Library => LibraryBooks 2021-09-02 15:26:20 -04:00
Robert McRackan
802763a4fb minor 2021-09-02 15:19:55 -04:00
Robert McRackan
b4803c42a5 Streamline GetLibrary_Flat_NoTracking with better context dispose 2021-09-02 14:51:06 -04:00
Robert McRackan
62c98c66a3 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-09-02 14:15:10 -04:00
Robert McRackan
6b289445e2 update dependencies 2021-09-02 14:14:25 -04:00
rmcrackan
52bf91f8aa Merge pull request #99 from Mbucari/master
Fixed crash when change RemoveBooksDialog checkbox via spacebar.
2021-09-02 13:47:57 -04:00
Michael Bucari-Tovo
6d2dff1a98 Code Cleanup 2021-09-02 11:21:20 -06:00
Mbucari
7c9970c0cb Merge branch 'rmcrackan:master' into master 2021-09-02 11:18:32 -06:00
Michael Bucari-Tovo
d2892f9076 Fix crash when checkbox checked via spacebar. 2021-09-02 11:11:40 -06:00
Robert McRackan
89f60a7ca3 fix wording 2021-09-02 11:35:32 -04:00
rmcrackan
ea37c09081 Merge pull request #98 from Mbucari/master
Added AAXClean as nuget package.
2021-09-02 10:54:42 -04:00
Michael Bucari-Tovo
76cb280933 Added AAXClean as nuget package. 2021-09-02 08:13:42 -06:00
Robert McRackan
0a54a8104c update counts label to reflect recent workflow changes 2021-09-02 09:54:02 -04:00
Robert McRackan
7464336535 remove WinFormsDesigner 2021-09-02 09:52:41 -04:00
Robert McRackan
dc0dd3474b separate the concepts of UserDefinedItem being updated in memory vs successful persistence 2021-09-02 09:51:17 -04:00
Robert McRackan
7b9c5c0f4f Add episode/podcast search engine bool 2021-09-01 16:56:09 -04:00
Robert McRackan
ad87f1851e Add episodes content type to Books in db 2021-09-01 16:51:59 -04:00
Robert McRackan
e8423341ef bug fix: bottom count numbers and menu options weren't updating on liberate-all 2021-09-01 14:28:01 -04:00
Robert McRackan
a9d3494af1 Added support for episodic content incl podcasts. Not yet complete. Need to mark them as such in the db somehow and also add search engine bools 2021-09-01 12:47:59 -04:00
Robert McRackan
90731a8948 'Convert all M4b to Mp3': add confirmation dialog with better explanation 2021-08-31 09:32:50 -04:00
Robert McRackan
e723467ca6 book liberation status Error:
* show system error icon in grid instead of stoplight
* list error count in bottom right #s
* SearchEngine bool: LiberatedError
2021-08-27 17:01:00 -04:00
Robert McRackan
722c33bf61 Add readme to help/annoy collaborators 2021-08-27 15:14:42 -04:00
Robert McRackan
f080215cbb Book details dialog: tags should get initial focus 2021-08-27 14:07:06 -04:00
Robert McRackan
d5c74d629f update dependencies 2021-08-27 11:16:13 -04:00
Robert McRackan
d12c246f6d version increm 2021-08-26 16:09:37 -04:00
Robert McRackan
8969c216af comments 2021-08-26 16:08:26 -04:00
Robert McRackan
9a4903f0dd Bug fix: after successful pdf download, this state wasn't being saved 2021-08-26 15:53:33 -04:00
Robert McRackan
3eda498a5e new AudibleApi nuget no longer relies on external json and js files which caused issues 2021-08-26 12:51:55 -04:00
Robert McRackan
8af7f28f04 (hopefully) final nuget pkg: Dinah.Core.WindowsDesktop 2021-08-26 12:49:37 -04:00
Robert McRackan
d9d7dfe1f7 update depandecies 2021-08-26 12:48:05 -04:00
Robert McRackan
b9c4d11946 remove TestCommon 2021-08-25 17:07:26 -04:00
Robert McRackan
68a5d7a58d nuget. done until I can figure out how to build .net5-windows nuget from github actions 2021-08-25 16:24:02 -04:00
Robert McRackan
4d69b222c5 nuget: Dinah.EntityFrameworkCore 2021-08-25 15:55:31 -04:00
Robert McRackan
42f94e7f6c more nuget migration 2021-08-25 15:33:09 -04:00
Robert McRackan
381d52be72 Better audible api to reduce captcha occurances 2021-08-24 13:42:14 -04:00
Robert McRackan
f16ad30891 bug fix from my last bug fix :( 2021-08-24 09:28:13 -04:00
Robert McRackan
ef53a6a8cb Bug fix: issue #92 2021-08-23 16:29:23 -04:00
Robert McRackan
9a37d434f1 FileLiberator is not db ignorant. It doesn't make context calls but still heavily uses the classes defined in the domain. Also uses internal util.s 2021-08-23 16:27:22 -04:00
Robert McRackan
d7eb190f69 reduce use of Book.Supplements 2021-08-23 16:16:08 -04:00
Robert McRackan
f19c46ee45 Book: add pdf url as is, not Absolute 2021-08-23 16:07:19 -04:00
rmcrackan
343c3b62d6 Merge pull request #90 from Mbucari/master
Fully implemented the MVVM pattern
2021-08-22 21:06:00 -04:00
Michael Bucari-Tovo
b1de10a71a Fix filtering. 2021-08-22 13:29:01 -06:00
Michael Bucari-Tovo
6beb5cc74a Made changes discussed. 2021-08-22 13:27:39 -06:00
Michael Bucari-Tovo
3767c3574a Merge branch 'master' of https://github.com/Mbucari/Libation 2021-08-21 22:09:27 -06:00
Michael Bucari-Tovo
4ceb4f9c03 Change back. 2021-08-21 22:09:13 -06:00
Mbucari
0f5149f7b4 Merge pull request #2 from rmcrackan/master
bug fix. race condition. can't check for book-exists-state before set…
2021-08-21 22:08:51 -06:00
Michael Bucari-Tovo
673451dc11 Git resolve 2021-08-21 22:08:35 -06:00
Michael Bucari-Tovo
e4257afc14 Version Num 2021-08-21 22:06:54 -06:00
Michael Bucari-Tovo
2a7e185dc3 Finish MVVM conversion 2021-08-21 22:03:16 -06:00
Michael Bucari-Tovo
9e06c343c1 Don't check if values have changed when updating the database. 2021-08-21 21:15:25 -06:00
Michael Bucari-Tovo
40b3a9990d FileLiberator is now DB ignorant. IProcessables update UserDaefinedData which notifies the view model. 2021-08-21 20:49:54 -06:00
Robert McRackan
d66c112a1e bug fix. race condition. can't check for book-exists-state before setting this state 2021-08-21 22:32:45 -04:00
Michael Bucari-Tovo
d826885728 Fix display for new LiberatedStatus values. 2021-08-21 18:37:07 -06:00
Michael Bucari-Tovo
263222d8cc Changed method signature. 2021-08-21 18:21:22 -06:00
Michael Bucari-Tovo
f25734334d Add separate command for updating tags 2021-08-21 18:16:24 -06:00
Michael Bucari-Tovo
ede8397f13 Needed to add check for actual file since Audio_Exists is now an application state. 2021-08-21 18:15:39 -06:00
Michael Bucari-Tovo
1369ee575a Replaced LiberatedState with LiberatedStatus and PdfState with LiberatedStatus? 2021-08-21 16:29:16 -06:00
Robert McRackan
c8e2418af7 incr ver for new liberation status handling 2021-08-21 14:04:48 -04:00
rmcrackan
2da25edafd Merge pull request #87 from Mbucari/master
Make sure DataGridView updates the display immediately after Details are changed.
2021-08-21 13:37:05 -04:00
Michael Bucari-Tovo
f60964f4c7 Unsubscribe IStreamable events from Disposed instead of FormClosed. 2021-08-21 08:47:24 -06:00
Michael Bucari-Tovo
3183f99153 Remove unnecessary overrides. 2021-08-21 08:45:43 -06:00
Michael Bucari-Tovo
2a22cff67c Revert "Fixed PDF download form disposed error"
This reverts commit 7fbe8ae769.
2021-08-21 08:14:10 -06:00
Michael Bucari-Tovo
7fbe8ae769 Fixed PDF download form disposed error 2021-08-21 08:03:40 -06:00
Robert McRackan
f9df466ad8 retain seeded file locations 2021-08-21 09:59:06 -04:00
Michael Bucari-Tovo
0b129fcf7c Fixed NetworkFileStream not resuming from cancellation. 2021-08-20 21:05:29 -06:00
Robert McRackan
2be5fd5af3 Omit '.libhack' skip/error files 2021-08-20 22:16:46 -04:00
Robert McRackan
c9727f84ab (hopefully) complete minimum viable product with stateful is-liberated status 2021-08-20 21:22:52 -04:00
Robert McRackan
aa56bb74a1 refactor out most of TransitionalFileLocator. Almost done with new stateful is-liberated paradigm 2021-08-20 20:51:37 -04:00
Michael Bucari-Tovo
85a6e21dcf Make sure network file isn't left open. 2021-08-20 17:03:15 -06:00
Michael Bucari-Tovo
8c620c25ab Separate concerns. 2021-08-20 16:10:05 -06:00
Michael Bucari-Tovo
813d91dfa4 Better naming 2021-08-20 15:40:16 -06:00
Michael Bucari-Tovo
d0d66c6135 Update using NotifyPropertyChanged instead of Row.Invalidate 2021-08-20 15:38:30 -06:00
Michael Bucari-Tovo
a8d609676e Null check. 2021-08-20 14:57:23 -06:00
Michael Bucari-Tovo
8386da5ec6 Make gridview update the row after details changed. 2021-08-20 14:56:52 -06:00
Michael Bucari-Tovo
f5089e7e29 Use local rowIndex instead of DataGridViewCell.RowIndex 2021-08-20 14:53:12 -06:00
Robert McRackan
a639857ec6 Book details changes liberated status in db and search engine. Minor changes to audible api to hopefully fix the weird log-in edge cases 2021-08-20 16:06:01 -04:00
Robert McRackan
35b5d7370c book details form now has a way for user to toggle is vs is-not liberated for book and pdf. NOT yet wired up 2021-08-20 15:21:43 -04:00
Robert McRackan
c9f988acf8 Book details form: quick and dirty 1st draft 2021-08-20 14:45:28 -04:00
Robert McRackan
6dfef09ea3 begin process of changing 'edit tags' => book details 2021-08-20 13:26:12 -04:00
rmcrackan
7e288c0c08 Merge pull request #86 from Mbucari/master
Use new Dinah.Core.Threading and remove unnecessary DataGridViewImageButtonColumn
2021-08-19 17:32:00 -04:00
Mbucari
dbcf6f25db Merge branch 'rmcrackan:master' into master 2021-08-19 15:14:56 -06:00
rmcrackan
88133652e9 unofficial linux/mac support 2021-08-19 11:17:42 -04:00
Robert McRackan
e768466943 Add hoopla script to repo. Currently unused. A Libation user passed it on to me. Notes are in _README.txt 2021-08-19 11:12:16 -04:00
Michael Bucari-Tovo
0cc55fd1e8 Widen Liberate column so sort arrow is shown. 2021-08-18 15:53:35 -06:00
Michael Bucari-Tovo
e36ea70cd1 Removed unnecessary class and simplified. 2021-08-18 15:53:09 -06:00
Michael Bucari-Tovo
a86185e644 Updated to use new Dinah.Core.Threading 2021-08-18 14:29:25 -06:00
Michael Bucari-Tovo
64a8f007a5 Use new Dinah.Core string extensions. 2021-08-18 14:11:25 -06:00
Michael Bucari-Tovo
215a626c92 SynchronizeInvoker => Dinah.Core.Threading.SynchronizeInvoker 2021-08-18 14:08:48 -06:00
Michael Bucari-Tovo
de93047192 Use new Dinah.Core.Threading 2021-08-18 14:08:11 -06:00
Robert McRackan
79c9a094b5 Liberate > "Convert all M4b to Mp3" : Visible=true 2021-08-18 15:24:18 -04:00
Robert McRackan
012a92ea30 SortableBindingList2 => Dinah.Core SortableBindingList 2021-08-18 15:06:52 -04:00
Robert McRackan
2e60d2accf Lots of churn over the last few weeks. For now, incrementing minor version. Better reviews to happen this week 2021-08-18 11:25:22 -04:00
Robert McRackan
565d34cec9 bottom numbers formatting 2021-08-18 11:22:47 -04:00
Robert McRackan
dd6967e88b minor NFO change 2021-08-18 11:17:59 -04:00
rmcrackan
fb7f57ab69 Merge pull request #85 from Mbucari/master
"F*ck it, we'll do it live!"
2021-08-18 11:03:23 -04:00
Mbucari
88253cdb55 Update LiberationBaseForm.cs 2021-08-16 10:08:21 -06:00
Michael Bucari-Tovo
560880b53d Fixed typos. 2021-08-16 08:04:03 -06:00
Michael Bucari-Tovo
27ae5facbe Improved PictureStorage thread safety and more intuitive naming. 2021-08-15 14:09:43 -06:00
Michael Bucari-Tovo
7a90d9fba9 Typo 2021-08-14 00:31:16 -06:00
Michael Bucari-Tovo
f74b0d78db Improved BackgroundFileSystem thread safety 2021-08-14 00:25:32 -06:00
Michael Bucari-Tovo
52fb0a27ce Code cleanup. 2021-08-13 23:18:52 -06:00
Michael Bucari-Tovo
7bdcf4eef0 Improved NetworkFileStream asynchronous operation. 2021-08-13 22:53:17 -06:00
Michael Bucari-Tovo
a44c46333f Improved cross threaded invocation. 2021-08-13 16:34:09 -06:00
Michael Bucari-Tovo
766d427b19 Improved cross thread execution and minor refactoring. 2021-08-13 10:19:43 -06:00
Michael Bucari-Tovo
0e7930f2b6 Removed intermediate class. 2021-08-12 18:50:51 -06:00
Michael Bucari-Tovo
081878b6f7 Remove IStreamProcessable. IProcessible inherits IStreamable. 2021-08-12 18:43:34 -06:00
Michael Bucari-Tovo
f925d10d2b Renamed. 2021-08-12 18:02:49 -06:00
Michael Bucari-Tovo
e37a2ccca9 Typos and small error correction. 2021-08-12 17:56:36 -06:00
Michael Bucari-Tovo
3e2d69606b Consolidated base forms 2021-08-12 17:43:46 -06:00
Michael Bucari-Tovo
2c20d03506 Refactored StreamBaseForm 2021-08-12 13:48:10 -06:00
Michael Bucari-Tovo
97730d1793 Removed unnecessary cast. 2021-08-12 13:14:45 -06:00
Michael Bucari-Tovo
5ab4183f9b Fixed event handling. 2021-08-12 12:47:02 -06:00
Michael Bucari-Tovo
7acaac7bd3 Fixed subscription ordering. 2021-08-12 12:28:58 -06:00
Michael Bucari-Tovo
448fd78b8f Added Dispose on IProcessable.Completed in case form is created but never shown. 2021-08-12 12:23:55 -06:00
Michael Bucari-Tovo
56a48c04bf Minor refactoring. 2021-08-12 10:39:55 -06:00
Michael Bucari-Tovo
65027fd001 Renamed data grid columns. 2021-08-12 09:21:58 -06:00
Michael Bucari-Tovo
f57a46c772 fixed mistake. 2021-08-12 00:19:30 -06:00
Michael Bucari-Tovo
a45ab61929 Typos. 2021-08-12 00:14:59 -06:00
Michael Bucari-Tovo
cd67e7136b Fixed control logic. 2021-08-12 00:13:33 -06:00
Michael Bucari-Tovo
265ad3a782 Minor refactoring. 2021-08-11 23:44:04 -06:00
Michael Bucari-Tovo
9f49a88000 Removed comments 2021-08-11 23:28:11 -06:00
Michael Bucari-Tovo
b5d941d479 Refactoring and documenting. 2021-08-11 23:22:45 -06:00
Michael Bucari-Tovo
79ed92f303 Refinment 2021-08-11 21:19:38 -06:00
Michael Bucari-Tovo
1c239dc546 More process control refinements 2021-08-11 21:07:07 -06:00
Michael Bucari-Tovo
687591e08e Refined changes to BookLiberation 2021-08-11 20:22:36 -06:00
Michael Bucari-Tovo
0045cf05ef Redesign DookLiberation control flow. 2021-08-11 18:08:38 -06:00
Michael Bucari-Tovo
963d632208 Removed space. 2021-08-10 21:14:33 -06:00
Michael Bucari-Tovo
9c1f620223 Revert indenting 2021-08-10 20:54:43 -06:00
Michael Bucari-Tovo
de75543b33 Removed WindowsDesktopUtilities 2021-08-10 20:39:11 -06:00
Michael Bucari-Tovo
689ffc71a2 Removed WindowsDesktopUtilities dependency. 2021-08-10 20:38:17 -06:00
Michael Bucari-Tovo
d795244247 Updated PictureCached event and removed dependence on WinAudibleImageServer 2021-08-10 20:33:15 -06:00
Michael Bucari-Tovo
4989cda93c Added synchronous Picture downloader. 2021-08-10 20:16:34 -06:00
Michael Bucari-Tovo
2f3c0e8a95 Revert "Removed WinFormsDesigner because it's not being used."
This reverts commit 54d24a7b09.
2021-08-10 18:26:21 -06:00
Michael Bucari-Tovo
560523b99d Revert "Removed WinFormsDesigner project."
This reverts commit d5e9e49517.
2021-08-10 18:26:10 -06:00
Michael Bucari-Tovo
d5e9e49517 Removed WinFormsDesigner project. 2021-08-10 17:06:58 -06:00
Michael Bucari-Tovo
54d24a7b09 Removed WinFormsDesigner because it's not being used. 2021-08-10 17:05:04 -06:00
Michael Bucari-Tovo
19a710e080 Fix xml formatting. 2021-08-10 16:36:46 -06:00
Michael Bucari-Tovo
7bdf71a29b Code Cleanup 2021-08-10 16:33:59 -06:00
Michael Bucari-Tovo
ef35c2aee9 Code Cleanup 2021-08-10 16:15:32 -06:00
Michael Bucari-Tovo
95766a43c5 Refactoring. 2021-08-10 14:57:18 -06:00
Michael Bucari-Tovo
e1dfefbadf Comments and renaming. 2021-08-10 10:17:02 -06:00
Michael Bucari-Tovo
f81552565a Re-added double buffering. 2021-08-10 00:33:24 -06:00
Michael Bucari-Tovo
957bec1c7f Removed excessive declarations. 2021-08-10 00:28:48 -06:00
Michael Bucari-Tovo
5c8ad72a5e Removed unnecessary using. 2021-08-10 00:24:21 -06:00
Michael Bucari-Tovo
0b1d513f50 Reorganized. 2021-08-10 00:18:43 -06:00
Michael Bucari-Tovo
d770109d86 Removed double buffering. 2021-08-10 00:15:07 -06:00
Michael Bucari-Tovo
235d0acede Removed unnecessary code. 2021-08-10 00:05:18 -06:00
Michael Bucari-Tovo
6f184273b8 Removed unnecessary declarations. 2021-08-09 23:48:32 -06:00
Michael Bucari-Tovo
a4cb934611 Updated resource file. 2021-08-09 23:31:59 -06:00
Michael Bucari-Tovo
6aefdfca9d Fixed tool strip menu for book removal. 2021-08-09 23:25:41 -06:00
Michael Bucari-Tovo
c7454ea5d2 Updated RemoveBooksDialog to use latest GridEntry 2021-08-09 23:11:37 -06:00
Michael Bucari-Tovo
2ef746a94c Add debug constants and don't check updates in debug.
Refactored cell formatting

Made GridEntry thread safe

Moved PictureStorage set defaults into constructor.
2021-08-09 22:28:07 -06:00
Michael Bucari-Tovo
ab82e7c99c Added comment. 2021-08-09 19:15:41 -06:00
Michael Bucari-Tovo
5f8ca9a0b5 Changed sort method. 2021-08-09 19:07:00 -06:00
Michael Bucari-Tovo
d48bd5ad07 Major UI refactoring. 2021-08-09 18:56:06 -06:00
Michael Bucari-Tovo
af48641281 Added remove books context menu item. 2021-08-09 01:08:58 -06:00
Michael Bucari-Tovo
f621ca63e8 Added RemoveBooksDialog 2021-08-09 01:08:35 -06:00
Michael Bucari-Tovo
35f54779f0 Add library command to get books removed from library. 2021-08-09 01:08:00 -06:00
Michael Bucari-Tovo
f68f374b78 Use resource user-agent. 2021-08-09 01:07:24 -06:00
rmcrackan
7e89386173 Merge pull request #80 from Mbucari/master
Fixed AAXC not downloading.
2021-08-03 09:17:03 -04:00
Michael Bucari-Tovo
7685613e8c Version update. 2021-08-03 06:42:48 -06:00
Michael Bucari-Tovo
727d1479bb Updated user-agent to fix AAXC files not downloading. 2021-08-03 06:41:44 -06:00
Michael Bucari-Tovo
bb46021f20 Fuxed possible null reference. 2021-08-03 06:41:22 -06:00
Robert McRackan
c45e6d526c New statuses added to export files: json, csv, excel 2021-07-29 15:11:42 -04:00
Robert McRackan
a72c3f069b ver # 2021-07-29 14:56:38 -04:00
Robert McRackan
1fcacb9cfb Much faster for grid refresh 2021-07-29 14:55:48 -04:00
Robert McRackan
a3542c53e2 Remove duplicate logic 2021-07-29 11:32:16 -04:00
Robert McRackan
9e44a95ba2 Double buffer grid 2021-07-29 10:57:55 -04:00
Robert McRackan
204e77008b TransitionalFileLocator to trust Book values, not hit db directly 2021-07-29 10:20:27 -04:00
402 changed files with 16728 additions and 11036 deletions

View File

@@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\AAXClean\AAXClean.csproj" />
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,232 +0,0 @@
using AAXClean;
using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO;
using Dinah.Core.Logging;
using Dinah.Core.StepRunner;
using System;
using System.IO;
namespace AaxDecrypter
{
public enum OutputFormat
{
Mp4a,
Mp3
}
public class AaxcDownloadConverter
{
public event EventHandler<AppleTags> RetrievedTags;
public event EventHandler<byte[]> RetrievedCoverArt;
public event EventHandler<int> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining;
public string AppName { get; set; } = nameof(AaxcDownloadConverter);
private string outputFileName { get; }
private string cacheDir { get; }
private DownloadLicense downloadLicense { get; }
private AaxFile aaxFile;
private byte[] coverArt;
private OutputFormat OutputFormat;
private StepSequence steps { get; }
private NetworkFileStreamPersister nfsPersister;
private bool isCanceled { get; set; }
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".aaxc");
public AaxcDownloadConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
outputFileName = outFileName;
var outDir = Path.GetDirectoryName(outputFileName);
if (!Directory.Exists(outDir))
throw new ArgumentNullException(nameof(outDir), "Directory does not exist");
if (File.Exists(outputFileName))
File.Delete(outputFileName);
if (!Directory.Exists(cacheDirectory))
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
cacheDir = cacheDirectory;
downloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
OutputFormat = outputFormat;
steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + (outputFormat == OutputFormat.Mp4a ? "M4b" : "Mp3"),
["Step 1: Get Aaxc Metadata"] = Step1_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step2_DownloadAndCombine,
["Step 3: Create Cue"] = Step3_CreateCue,
["Step 4: Create Nfo"] = Step4_CreateNfo,
["Step 5: Cleanup"] = Step5_Cleanup,
};
}
public void SetCoverArt(byte[] coverArt)
{
if (coverArt is null) return;
this.coverArt = coverArt;
RetrievedCoverArt?.Invoke(this, coverArt);
}
public bool Run()
{
var (IsSuccess, Elapsed) = steps.Run();
if (!IsSuccess)
{
Console.WriteLine("WARNING-Conversion failed");
return false;
}
var speedup = (int)(aaxFile.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
Console.WriteLine("Speedup is " + speedup + "x realtime.");
Console.WriteLine("Done");
return true;
}
public bool Step1_GetMetadata()
{
//Get metadata from the file over http
if (File.Exists(jsonDownloadState))
{
try
{
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState);
//If More thaan ~1 hour has elapsed since getting the download url, it will expire.
//The new url will be to the same file.
nfsPersister.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
}
catch
{
FileExt.SafeDelete(jsonDownloadState);
FileExt.SafeDelete(tempFile);
nfsPersister = NewNetworkFilePersister();
}
}
else
{
nfsPersister = NewNetworkFilePersister();
}
nfsPersister.NetworkFileStream.BeginDownloading();
aaxFile = new AaxFile(nfsPersister.NetworkFileStream);
coverArt = aaxFile.AppleTags.Cover;
RetrievedTags?.Invoke(this, aaxFile.AppleTags);
RetrievedCoverArt?.Invoke(this, coverArt);
return !isCanceled;
}
private NetworkFileStreamPersister NewNetworkFilePersister()
{
var headers = new System.Net.WebHeaderCollection
{
{ "User-Agent", downloadLicense.UserAgent }
};
var networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
public bool Step2_DownloadAndCombine()
{
DecryptProgressUpdate?.Invoke(this, 0);
if (File.Exists(outputFileName))
FileExt.SafeDelete(outputFileName);
FileStream outFile = File.OpenWrite(outputFileName);
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV);
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
var decryptionResult = OutputFormat == OutputFormat.Mp4a ? aaxFile.ConvertToMp4a(outFile, downloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outFile);
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
aaxFile.Close();
downloadLicense.ChapterInfo = aaxFile.Chapters;
if (decryptionResult == ConversionResult.NoErrorsDetected
&& coverArt is not null
&& OutputFormat == OutputFormat.Mp4a)
{
//This handles a special case where the aaxc file doesn't contain cover art and
//Libation downloaded it instead (Animal Farm). Currently only works for Mp4a files.
using var decryptedBook = new Mp4File(outputFileName, FileAccess.ReadWrite);
decryptedBook.AppleTags?.SetCoverArt(coverArt);
decryptedBook.Save();
decryptedBook.Close();
}
nfsPersister.Dispose();
DecryptProgressUpdate?.Invoke(this, 0);
return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled;
}
private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{
var duration = aaxFile.Duration;
double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining))
DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
DecryptProgressUpdate?.Invoke(this, (int)progressPercent);
}
public bool Step3_CreateCue()
{
// not a critical step. its failure should not prevent future steps from running
try
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Step3_CreateCue)}. FAILED");
}
return !isCanceled;
}
public bool Step4_CreateNfo()
{
// not a critical step. its failure should not prevent future steps from running
try
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxFile, downloadLicense.ChapterInfo));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Step4_CreateNfo)}. FAILED");
}
return !isCanceled;
}
public bool Step5_Cleanup()
{
FileExt.SafeDelete(jsonDownloadState);
FileExt.SafeDelete(tempFile);
return !isCanceled;
}
public void Cancel()
{
isCanceled = true;
aaxFile?.Cancel();
}
}
}

View File

@@ -1,27 +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)
{
ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
ArgumentValidator.EnsureNotNullOrEmpty(audibleKey, nameof(audibleKey));
ArgumentValidator.EnsureNotNullOrEmpty(audibleIV, nameof(audibleIV));
ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
DownloadUrl = downloadUrl;
AudibleKey = audibleKey;
AudibleIV = audibleIV;
UserAgent = userAgent;
}
}
}

View File

@@ -1,54 +0,0 @@
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public static class NFO
{
public static string CreateContents(string ripper, AAXClean.Mp4File aaxcTagLib, ChapterInfo chapters)
{
var _hours = (int)aaxcTagLib.Duration.TotalHours;
var myDuration
= (_hours > 0 ? _hours + " hours, " : string.Empty)
+ aaxcTagLib.Duration.Minutes + " minutes, "
+ aaxcTagLib.Duration.Seconds + " seconds";
var nfoString
= "General Information\r\n"
+ "======================\r\n"
+ $" Title: {aaxcTagLib.AppleTags.TitleSansUnabridged?.UnicodeToAscii() ?? "[unknown]"}\r\n"
+ $" Author: {aaxcTagLib.AppleTags.FirstAuthor?.UnicodeToAscii() ?? "[unknown]"}\r\n"
+ $" Read By: {aaxcTagLib.AppleTags.Narrator?.UnicodeToAscii() ?? "[unknown]"}\r\n"
+ $" Release Date: {aaxcTagLib.AppleTags.ReleaseDate ?? "[unknown]"}\r\n"
+ $" Book Copyright: {aaxcTagLib.AppleTags.BookCopyright ?? "[unknown]"}\r\n"
+ $" Recording Copyright: {aaxcTagLib.AppleTags.RecordingCopyright ?? "[unknown]"}\r\n"
+ $" Genre: {aaxcTagLib.AppleTags.Generes ?? "[unknown]"}\r\n"
+ $" Publisher: {aaxcTagLib.AppleTags.Publisher ?? "[unknown]"}\r\n"
+ $" Duration: {myDuration}\r\n"
+ $" Chapters: {chapters.Count}\r\n"
+ "\r\n"
+ "\r\n"
+ "Media Information\r\n"
+ "======================\r\n"
+ " Source Format: Audible AAX\r\n"
+ $" Source Sample Rate: {aaxcTagLib.TimeScale} Hz\r\n"
+ $" Source Channels: {aaxcTagLib.AudioChannels}\r\n"
+ $" Source Bitrate: {aaxcTagLib.AverageBitrate} Kbps\r\n"
+ "\r\n"
+ " Lossless Encode: Yes\r\n"
+ " Encoded Codec: AAC / M4B\r\n"
+ $" Encoded Sample Rate: {aaxcTagLib.TimeScale} Hz\r\n"
+ $" Encoded Channels: {aaxcTagLib.AudioChannels}\r\n"
+ $" Encoded Bitrate: {aaxcTagLib.AverageBitrate} Kbps\r\n"
+ "\r\n"
+ $" Ripper: {ripper}\r\n"
+ "\r\n"
+ "\r\n"
+ "Book Description\r\n"
+ "================\r\n"
+ (!string.IsNullOrWhiteSpace(aaxcTagLib.AppleTags.LongDescription) ? aaxcTagLib.AppleTags.LongDescription.UnicodeToAscii() : aaxcTagLib.AppleTags.Comment?.UnicodeToAscii());
return nfoString;
}
}
}

View File

@@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.1.1" />
<PackageReference Include="NPOI" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
<ProjectReference Include="..\DtoImporterService\DtoImporterService.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
<ProjectReference Include="..\LibationSearchEngine\LibationSearchEngine.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,16 +0,0 @@
using System;
using DataLayer;
using FileManager;
namespace ApplicationServices
{
public static class DbContexts
{
//// idea for future command/query separation
// public static LibationContext GetCommandContext() { }
// public static LibationContext GetQueryContext() { }
public static LibationContext GetContext()
=> LibationContext.Create(SqliteStorage.ConnectionString);
}
}

View File

@@ -1,214 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using DataLayer;
using Dinah.Core;
using DtoImporterService;
using InternalUtilities;
using Serilog;
namespace ApplicationServices
{
public static class LibraryCommands
{
#region FULL LIBRARY scan and import
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts)
{
if (accounts is null || accounts.Length == 0)
return (0, 0);
try
{
var importItems = await scanAccountsAsync(loginCallbackFactoryFunc, accounts);
var totalCount = importItems.Count;
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
var newCount = await importIntoDbAsync(importItems);
Log.Logger.Information($"Import: New count {newCount}");
await Task.Run(() => SearchEngineCommands.FullReIndex());
Log.Logger.Information("FullReIndex: success");
return (totalCount, newCount);
}
catch (AudibleApi.Authentication.LoginFailedException lfEx)
{
lfEx.SaveFiles(FileManager.Configuration.Instance.LibationFiles);
// nuget Serilog.Exceptions would automatically log custom properties
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
// https://github.com/RehanSaeed/Serilog.Exceptions
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new
{
lfEx.RequestUrl,
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
lfEx.ResponseInputFields,
lfEx.ResponseBodyFilePaths
});
throw;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error importing library");
throw;
}
}
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, Account[] accounts)
{
var tasks = new List<Task<List<ImportItem>>>();
foreach (var account in accounts)
{
var callback = loginCallbackFactoryFunc(account);
// get APIs in serial, esp b/c of logins
var api = await AudibleApiActions.GetApiAsync(callback, account);
// add scanAccountAsync as a TASK: do not await
tasks.Add(scanAccountAsync(api, account));
}
// import library in parallel
var arrayOfLists = await Task.WhenAll(tasks);
var importItems = arrayOfLists.SelectMany(a => a).ToList();
return importItems;
}
private static async Task<List<ImportItem>> scanAccountAsync(Api api, Account account)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
{
Account = account?.MaskedLogEntry ?? "[null]"
});
var dtoItems = await AudibleApiActions.GetLibraryValidatedAsync(api);
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
}
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
{
using var context = DbContexts.GetContext();
var libraryImporter = new LibraryImporter(context);
var newCount = await Task.Run(() => libraryImporter.Import(importItems));
context.SaveChanges();
return newCount;
}
#endregion
#region Update book details
public static int UpdateTags(Book book, string newTags)
{
try
{
using var context = DbContexts.GetContext();
var udi = book.UserDefinedItem;
// Attach() NoTracking entities before SaveChanges()
udi.Tags = newTags;
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
SearchEngineCommands.UpdateBookTags(book);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
throw;
}
}
public static int UpdateBook(LibraryBook libraryBook, LiberatedStatus liberatedStatus, string finalAudioPath)
{
try
{
using var context = DbContexts.GetContext();
var udi = libraryBook.Book.UserDefinedItem;
// Attach() NoTracking entities before SaveChanges()
udi.BookStatus = liberatedStatus;
udi.BookLocation = finalAudioPath;
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
SearchEngineCommands.UpdateLiberatedStatus(libraryBook.Book);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
throw;
}
}
public static int UpdatePdf(LibraryBook libraryBook, LiberatedStatus liberatedStatus)
{
try
{
using var context = DbContexts.GetContext();
var udi = libraryBook.Book.UserDefinedItem;
// Attach() NoTracking entities before SaveChanges()
udi.PdfStatus = liberatedStatus;
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
throw;
}
}
#endregion
// this is a query, not command so maybe I should make a LibraryQueries. except there's already one of those...
private enum AudioFileState { full, aax, none }
private static AudioFileState getAudioFileState(string productId)
=> TransitionalFileLocator.Audio_Exists(productId) ? AudioFileState.full
: TransitionalFileLocator.AAXC_Exists(productId) ? AudioFileState.aax
: AudioFileState.none;
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { }
public static LibraryStats GetCounts()
{
var libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
var results = libraryBooks
.AsParallel()
.Select(lb => getAudioFileState(lb.Book.AudibleProductId))
.ToList();
var booksFullyBackedUp = results.Count(r => r == AudioFileState.full);
var booksDownloadedOnly = results.Count(r => r == AudioFileState.aax);
var booksNoProgress = results.Count(r => r == AudioFileState.none);
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress });
var boolResults = libraryBooks
.AsParallel()
.Where(lb => lb.Book.Supplements.Any())
.Select(lb => TransitionalFileLocator.PDF_Exists(lb.Book.AudibleProductId))
.ToList();
var pdfsDownloaded = boolResults.Count(r => r);
var pdfsNotDownloaded = boolResults.Count(r => !r);
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, pdfsDownloaded, pdfsNotDownloaded);
}
}
}

View File

@@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DataLayer;
using FileManager;
namespace ApplicationServices
{
public static class TransitionalFileLocator
{
public static string Audio_GetPath(string productId)
{
var book = DbContexts.GetContext().GetBook_Flat_NoTracking(productId);
var loc = book?.UserDefinedItem?.BookLocation ?? "";
if (File.Exists(loc))
return loc;
return AudibleFileStorage.Audio.GetPath(productId);
}
public static bool PDF_Exists(string productId)
{
var book = DbContexts.GetContext().GetBook_Flat_NoTracking(productId);
var status = book?.UserDefinedItem?.PdfStatus;
if (status.HasValue && status.Value == LiberatedStatus.Liberated)
return true;
return AudibleFileStorage.PDF.Exists(productId);
}
public static bool Audio_Exists(string productId)
{
var book = DbContexts.GetContext().GetBook_Flat_NoTracking(productId);
var status = book?.UserDefinedItem?.BookStatus;
// true since Error == libhack
if (status != LiberatedStatus.NotLiberated)
return true;
return AudibleFileStorage.Audio.Exists(productId);
}
public static bool AAXC_Exists(string productId)
{
// this one will actually stay the same. centralizing helps with organization in the interim though
return AudibleFileStorage.AAXC.Exists(productId);
}
}
}

View File

@@ -1,18 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class LibraryBookConfig : IEntityTypeConfiguration<LibraryBook>
{
public void Configure(EntityTypeBuilder<LibraryBook> entity)
{
entity.HasKey(b => b.BookId);
entity
.HasOne(le => le.Book)
.WithOne()
.HasForeignKey<LibraryBook>(le => le.BookId);
}
}
}

View File

@@ -1,40 +0,0 @@
using Dinah.Core;
namespace DataLayer
{
public class SeriesBook
{
internal int SeriesId { get; private set; }
internal int BookId { get; private set; }
/// <summary>
/// <para>"index" not "order". This is both for sequence and display</para>
/// <para>Float allows for in-between books. eg: 2.5</para>
/// <para>To show 2 editions as the same book in a series, give them the same index</para>
/// <para>null IS NOT the same as 0. Some series call a book "book 0"</para>
/// </summary>
public float? Index { get; private set; }
public Series Series { get; private set; }
public Book Book { get; private set; }
private SeriesBook() { }
internal SeriesBook(Series series, Book book, float? index = null)
{
ArgumentValidator.EnsureNotNull(series, nameof(series));
ArgumentValidator.EnsureNotNull(book, nameof(book));
Series = series;
Book = book;
Index = index;
}
public void UpdateIndex(float? index)
{
if (index.HasValue)
Index = index.Value;
}
public override string ToString() => $"Series={Series} Book={Book}";
}
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Linq;
namespace DataLayer
{
public static class GenericPaging
{
public static IQueryable<T> Page<T>(this IQueryable<T> query, int pageNumZeroStart, int pageSize)
{
if (pageSize < 1)
throw new ArgumentOutOfRangeException(nameof(pageSize), "pageSize must be at least 1");
if (pageNumZeroStart > 0)
query = query.Skip(pageNumZeroStart * pageSize);
return query.Take(pageSize);
}
}
}

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,57 +0,0 @@
FOR QUICK MIGRATION INSTRUCTIONS:
_DB_NOTES.txt
HOW TO CREATE: EF CORE PROJECT
==============================
example is for sqlite but the same works with MsSql
nuget
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
Microsoft.EntityFrameworkCore.Sqlite
MIGRATIONS
require core, not standard
this can be a problem b/c standard and framework can only reference standard, not core
TO USE MIGRATIONS (core and/or standard)
add to csproj
<PropertyGroup>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
TO USE MIGRATIONS AS *BOTH* CORE AND STANDARD
edit csproj
pluralize this xml tag
from: TargetFramework
to: TargetFrameworks
inside of TargetFrameworks
from: netstandard2.1
to: netcoreapp3.1;netstandard2.1
run. error
SQLite Error 1: 'no such table: Blogs'.
set project "Set as StartUp Project"
Tools >> Nuget Package Manager >> Package Manager Console
default project: Examples\SQLite_NETCore2_0
PM> add-migration InitialCreate
PM> Update-Database
if add-migration xyz throws and error, don't take the error msg at face value. try again with add-migration xyz -verbose
new sqlite .db file created: Copy always/Copy if newer
or copy .db file to destination
relative:
optionsBuilder.UseSqlite("Data Source=blogging.db");
absolute (use fwd slashes):
optionsBuilder.UseSqlite("Data Source=C:/foo/bar/blogging.db");
REFERENCE ARTICLES
------------------
https://docs.microsoft.com/en-us/ef/core/get-started/netcore/new-db-sqlite
https://carlos.mendible.com/2016/07/11/step-by-step-dotnet-core-and-entity-framework-core/
https://www.benday.com/2017/12/19/ef-core-2-0-migrations-without-hard-coded-connection-strings/

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/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)

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,103 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
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)
{
//// 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);
var remainingContribNames = contributorNames
.Distinct()
.Except(localNames)
.ToList();
// load existing => local
// remember to include default/empty/missing
var emptyName = Contributor.GetEmpty().Name;
if (remainingContribNames.Any())
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name) || c.Name == emptyName).ToList();
}
// only use after loading contributors => local
private int upsertPeople(List<Person> people)
{
var qtyNew = 0;
foreach (var p in people)
{
// Should be 'Single' not 'First'. A user had a duplicate get in somehow though so I'm now using 'First' defensively
var person = DbContext.Contributors.Local.FirstOrDefault(c => c.Name == p.Name);
if (person == null)
{
person = DbContext.Contributors.Add(new Contributor(p.Name, p.Asin)).Entity;
qtyNew++;
}
}
return qtyNew;
}
// only use after loading contributors => local
private int upsertPublishers(List<string> publishers)
{
var qtyNew = 0;
foreach (var publisherName in publishers)
{
if (DbContext.Contributors.Local.SingleOrDefault(c => c.Name == publisherName) == null)
{
DbContext.Contributors.Add(new Contributor(publisherName));
qtyNew++;
}
}
return qtyNew;
}
}
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{
public class LibraryImporter : ItemsImporterBase
{
public LibraryImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new LibraryValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
new BookImporter(DbContext).Import(importItems);
var qtyNew = upsertLibraryBooks(importItems);
return qtyNew;
}
private int upsertLibraryBooks(IEnumerable<ImportItem> importItems)
{
// technically, we should be able to have duplicate books from separate accounts.
// this would violate the current pk and would be difficult to deal with elsewhere:
// - what to show in the grid
// - which to consider liberated
//
// sqlite cannot alter pk. the work around is an extensive headache. it'll be fixed in pre .net5/efcore5
//
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
//
// CURRENT SOLUTION: don't re-insert
var currentLibraryProductIds = DbContext.Library.Select(l => l.Book.AudibleProductId).ToList();
var newItems = importItems.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId)).ToList();
foreach (var newItem in newItems)
{
var libraryBook = new LibraryBook(
DbContext.Books.Local.Single(b => b.AudibleProductId == newItem.DtoItem.ProductId),
newItem.DtoItem.DateAdded,
newItem.AccountId);
DbContext.Library.Add(libraryBook);
}
// needed for v3 => v4 upgrade
var toUpdate = DbContext.Library.Where(l => l.Account == null);
foreach (var u in toUpdate)
{
var item = importItems.FirstOrDefault(ii => ii.DtoItem.ProductId == u.Book.AudibleProductId);
if (item != null)
u.UpdateAccount(item.AccountId);
}
var qtyNew = newItems.Count;
return qtyNew;
}
}
}

View File

@@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
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<AudibleApiDTOs.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<AudibleApiDTOs.Series> requestedSeries)
{
var qtyNew = 0;
foreach (var s in requestedSeries)
{
var series = DbContext.Series.Local.SingleOrDefault(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,57 +0,0 @@
using System;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
using FileManager;
namespace FileLiberator
{
/// <summary>
/// Download DRM book and decrypt audiobook files
///
/// Processes:
/// Download: download aax file: the DRM encrypted audiobook
/// Decrypt: remove DRM encryption from audiobook. Store final book
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
/// </summary>
public class BackupBook : IProcessable
{
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed;
public DownloadDecryptBook DownloadDecryptBook { get; } = new DownloadDecryptBook();
public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
public bool Validate(LibraryBook libraryBook)
=> !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book.AudibleProductId);
// do NOT use ConfigureAwait(false) on ProcessAsync()
// often calls events which prints to forms in the UI context
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
Begin?.Invoke(this, libraryBook);
try
{
{
var statusHandler = await DownloadDecryptBook.TryProcessAsync(libraryBook);
if (statusHandler.HasErrors)
return statusHandler;
}
{
var statusHandler = await DownloadPdf.TryProcessAsync(libraryBook);
if (statusHandler.HasErrors)
return statusHandler;
}
return new StatusHandler();
}
finally
{
Completed?.Invoke(this, libraryBook);
}
}
}
}

View File

@@ -1,98 +0,0 @@
using AAXClean;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using Dinah.Core.IO;
using FileManager;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace FileLiberator
{
public class ConvertToMp3 : IDecryptable
{
public event EventHandler<string> DecryptBegin;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
public event EventHandler<int> UpdateProgress;
public event EventHandler<TimeSpan> UpdateRemainingTime;
public event EventHandler<string> DecryptCompleted;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> StatusUpdate;
public event EventHandler<Action<byte[]>> RequestCoverArt;
private Mp4File m4bBook;
private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
public void Cancel() => m4bBook?.Cancel();
public bool Validate(LibraryBook libraryBook)
{
var path = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book.AudibleProductId);
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
}
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
Begin?.Invoke(this, libraryBook);
DecryptBegin?.Invoke(this, $"Begin converting {libraryBook} to mp3");
try
{
var m4bPath = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book.AudibleProductId);
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
TitleDiscovered?.Invoke(this, m4bBook.AppleTags.Title);
AuthorsDiscovered?.Invoke(this, m4bBook.AppleTags.FirstAuthor);
NarratorsDiscovered?.Invoke(this, m4bBook.AppleTags.Narrator);
CoverImageFilepathDiscovered?.Invoke(this, m4bBook.AppleTags.Cover);
using var mp3File = File.OpenWrite(Path.GetTempFileName());
var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File));
m4bBook.InputStream.Close();
mp3File.Close();
var mp3Path = Mp3FileName(m4bPath);
FileExt.SafeMove(mp3File.Name, mp3Path);
var statusHandler = new StatusHandler();
if (result == ConversionResult.Failed)
statusHandler.AddError("Conversion failed");
return statusHandler;
}
finally
{
DecryptCompleted?.Invoke(this, $"Completed converting to mp3: {libraryBook.Book.Title}");
Completed?.Invoke(this, libraryBook);
}
}
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{
var duration = m4bBook.Duration;
double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining))
UpdateRemainingTime?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
UpdateProgress?.Invoke(this, (int)progressPercent);
}
}
}

View File

@@ -1,229 +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;
namespace FileLiberator
{
public class DownloadDecryptBook : IDecryptable
{
public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> DecryptBegin;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
public event EventHandler<int> UpdateProgress;
public event EventHandler<TimeSpan> UpdateRemainingTime;
public event EventHandler<string> DecryptCompleted;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> StatusUpdate;
private AaxcDownloadConverter aaxcDownloader;
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
Begin?.Invoke(this, libraryBook);
try
{
if (ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book.AudibleProductId))
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
// decrypt failed
if (outputAudioFilename is null)
return new StatusHandler { "Decrypt failed" };
// moves files and returns dest dir
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var finalAudioExists = ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book.AudibleProductId);
if (!finalAudioExists)
return new StatusHandler { "Cannot find final audio file after decryption" };
// only need to update if success. if failure, it will remain at 0 == NotLiberated
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Liberated, outputAudioFilename);
return new StatusHandler();
}
finally
{
Completed?.Invoke(this, libraryBook);
}
}
private async Task<string> aaxToM4bConverterDecryptAsync(string cacheDir, string destinationDir, LibraryBook libraryBook)
{
DecryptBegin?.Invoke(this, $"Begin decrypting {libraryBook}");
try
{
validate(libraryBook);
var api = await InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
var aaxcDecryptDlLic = new DownloadLicense
(
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
contentLic?.Voucher?.Key,
contentLic?.Voucher?.Iv,
Resources.UserAgent
);
if (Configuration.Instance.AllowLibationFixup)
{
aaxcDecryptDlLic.ChapterInfo = new AAXClean.ChapterInfo();
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
aaxcDecryptDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
}
var format = Configuration.Instance.DecryptToLossy ? OutputFormat.Mp3 : OutputFormat.Mp4a;
var extension = format switch
{
OutputFormat.Mp4a => "m4b",
OutputFormat.Mp3 => "mp3",
_ => throw new NotImplementedException(),
};
var outFileName = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{extension}");
aaxcDownloader = new AaxcDownloadConverter(outFileName, cacheDir, aaxcDecryptDlLic, format) { AppName = "Libation" };
aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining);
aaxcDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
aaxcDownloader.RetrievedTags += aaxcDownloader_RetrievedTags;
// REAL WORK DONE HERE
var success = await Task.Run(() => aaxcDownloader.Run());
// decrypt failed
if (!success)
return null;
return outFileName;
}
finally
{
DecryptCompleted?.Invoke(this, $"Completed downloading and decrypting {libraryBook.Book.Title}");
}
}
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{
if (e is null && Configuration.Instance.AllowLibationFixup)
{
RequestCoverArt?.Invoke(this, aaxcDownloader.SetCoverArt);
}
if (e is not null)
{
CoverImageFilepathDiscovered?.Invoke(this, e);
}
}
private void aaxcDownloader_RetrievedTags(object sender, AAXClean.AppleTags e)
{
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
}
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
Directory.CreateDirectory(destinationDir);
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
foreach (var f in sortedFiles)
{
var dest
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
? audioFileName
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
Cue.UpdateFileName(f, audioFileName);
File.Move(f.FullName, dest);
}
AudibleFileStorage.Audio.Refresh();
return destinationDir;
}
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
{
// files are: temp path\author\[asin].ext
var m4bDir = new FileInfo(outputAudioFilename).Directory;
var files = m4bDir
.EnumerateFiles()
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
.ToList();
// move audio files to the end of the collection so these files are moved last
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
var sortedFiles = files
.Except(musicFiles)
.Concat(musicFiles)
.ToList();
return sortedFiles;
}
private static void validate(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"));
}
public bool Validate(LibraryBook libraryBook)
=> !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book.AudibleProductId);
public void Cancel()
{
aaxcDownloader?.Cancel();
}
}
}

View File

@@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
using FileManager;
namespace FileLiberator
{
public class DownloadPdf : DownloadableBase
{
public override bool Validate(LibraryBook libraryBook)
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book.AudibleProductId);
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(libraryBook);
var liberatedStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
ApplicationServices.LibraryCommands.UpdatePdf(libraryBook, liberatedStatus);
return result;
}
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
{
// if audio file exists, get it's dir. else return base Book dir
var existingPath = Path.GetDirectoryName(ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book.AudibleProductId));
var file = getdownloadUrl(libraryBook);
if (existingPath != null)
return Path.Combine(existingPath, Path.GetFileName(file));
var full = FileUtility.GetValidFilename(
AudibleFileStorage.PDF.StorageDirectory,
libraryBook.Book.Title,
Path.GetExtension(file),
libraryBook.Book.AudibleProductId);
return full;
}
private static string getdownloadUrl(LibraryBook libraryBook)
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
{
var api = await GetApiAsync(libraryBook);
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
var client = new HttpClient();
var actualDownloadedFilePath = await PerformDownloadAsync(
proposedDownloadFilePath,
(p) => client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, p));
}
private static StatusHandler verifyDownload(LibraryBook libraryBook)
=> !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book.AudibleProductId)
? new StatusHandler { "Downloaded PDF cannot be found" }
: new StatusHandler();
}
}

View File

@@ -1,64 +0,0 @@
using System;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
namespace FileLiberator
{
public abstract class DownloadableBase : IDownloadableProcessable
{
public event EventHandler<LibraryBook> Begin;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> DownloadBegin;
public event EventHandler<DownloadProgress> DownloadProgressChanged;
public event EventHandler<string> DownloadCompleted;
public event EventHandler<string> StatusUpdate;
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
public abstract bool Validate(LibraryBook libraryBook);
public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook);
// do NOT use ConfigureAwait(false) on ProcessAsync()
// often calls events which prints to forms in the UI context
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
Begin?.Invoke(this, libraryBook);
try
{
return await ProcessItemAsync(libraryBook);
}
finally
{
Completed?.Invoke(this, libraryBook);
}
}
protected static Task<AudibleApi.Api> GetApiAsync(LibraryBook libraryBook)
=> InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
{
var progress = new Progress<DownloadProgress>();
progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e);
DownloadBegin?.Invoke(this, proposedDownloadFilePath);
try
{
var result = await func(progress);
StatusUpdate?.Invoke(this, result);
return result;
}
finally
{
DownloadCompleted?.Invoke(this, proposedDownloadFilePath);
}
}
}
}

View File

@@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,20 +0,0 @@
using System;
namespace FileLiberator
{
public interface IDecryptable : IProcessable
{
event EventHandler<string> DecryptBegin;
event EventHandler<Action<byte[]>> RequestCoverArt;
event EventHandler<string> TitleDiscovered;
event EventHandler<string> AuthorsDiscovered;
event EventHandler<string> NarratorsDiscovered;
event EventHandler<byte[]> CoverImageFilepathDiscovered;
event EventHandler<int> UpdateProgress;
event EventHandler<TimeSpan> UpdateRemainingTime;
event EventHandler<string> DecryptCompleted;
void Cancel();
}
}

View File

@@ -1,12 +0,0 @@
using System;
using Dinah.Core.Net.Http;
namespace FileLiberator
{
public interface IDownloadable
{
event EventHandler<string> DownloadBegin;
event EventHandler<DownloadProgress> DownloadProgressChanged;
event EventHandler<string> DownloadCompleted;
}
}

View File

@@ -1,4 +0,0 @@
namespace FileLiberator
{
public interface IDownloadableProcessable : IDownloadable, IProcessable { }
}

View File

@@ -1,23 +0,0 @@
using System;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
namespace FileLiberator
{
public interface IProcessable
{
event EventHandler<LibraryBook> Begin;
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
event EventHandler<string> StatusUpdate;
event EventHandler<LibraryBook> Completed;
/// <returns>True == Valid</returns>
bool Validate(LibraryBook libraryBook);
/// <returns>True == success</returns>
Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);
}
}

View File

@@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
namespace FileLiberator
{
public static class IProcessableExt
{
//
// DO NOT USE ConfigureAwait(false) WITH ProcessAsync() unless ensuring ProcessAsync() implementation is cross-thread compatible
// ProcessAsync() often does a lot with forms in the UI context
//
// when used in foreach: stateful. deferred execution
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable)
=> DbContexts.GetContext()
.GetLibrary_Flat_NoTracking()
.Where(libraryBook => processable.Validate(libraryBook));
public static LibraryBook GetSingleLibraryBook(string productId)
{
using var context = DbContexts.GetContext();
var libraryBook = context.GetLibraryBook_Flat_NoTracking(productId);
return libraryBook;
}
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook)
{
if (!processable.Validate(libraryBook))
return new StatusHandler { "Validation failed" };
return await processable.ProcessBookAsync_NoValidation(libraryBook);
}
public static async Task<StatusHandler> ProcessBookAsync_NoValidation(this IProcessable processable, LibraryBook libraryBook)
{
Serilog.Log.Logger.Information("Begin " + nameof(ProcessBookAsync_NoValidation) + " {@DebugInfo}", new
{
libraryBook.Book.Title,
libraryBook.Book.AudibleProductId,
libraryBook.Book.Locale,
Account = libraryBook.Account?.ToMask() ?? "[empty]"
});
var status
= (await processable.ProcessAsync(libraryBook))
?? new StatusHandler { "Processable should never return a null status" };
return status;
}
public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
=> processable.Validate(libraryBook)
? await processable.ProcessAsync(libraryBook)
: new StatusHandler();
}
}

View File

@@ -1,163 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dinah.Core;
using Dinah.Core.Collections.Generic;
namespace FileManager
{
public enum FileType { Unknown, Audio, AAXC, PDF }
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
{
protected abstract string[] Extensions { get; }
public abstract string StorageDirectory { get; }
#region static
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static AudibleFileStorage AAXC { get; } = new AaxcFileStorage();
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
public static string BooksDirectory
{
get
{
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
Configuration.Instance.Books = Path.Combine(Configuration.UserProfile, "Books");
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
}
}
private static BackgroundFileSystem BookDirectoryFiles { get; } = new BackgroundFileSystem();
#endregion
#region instance
public FileType FileType => (FileType)Value;
private IEnumerable<string> extensions_noDots { get; }
private string extAggr { get; }
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
{
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
}
public void Refresh()
{
BookDirectoryFiles.RefreshFiles();
}
/// <summary>
/// Example for full books:
/// Search recursively in _books directory. Full book exists if either are true
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public bool Exists(string productId) => GetPath(productId) != null;
public string GetPath(string productId)
{
var cachedFile = FilePathCache.GetPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
string storageDir = StorageDirectory;
string regexPattern = $@"{productId}.*?\.({extAggr})$";
string firstOrNull;
if (storageDir == BooksDirectory)
{
//If user changed the BooksDirectory, reinitialize.
if (storageDir != BookDirectoryFiles.RootDirectory)
BookDirectoryFiles.Init(storageDir, "*.*", SearchOption.AllDirectories);
firstOrNull = BookDirectoryFiles.FindFile(regexPattern, RegexOptions.IgnoreCase);
}
else
{
firstOrNull =
Directory
.EnumerateFiles(storageDir, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, RegexOptions.IgnoreCase));
}
if (firstOrNull is null)
return null;
FilePathCache.Upsert(productId, FileType, firstOrNull);
return firstOrNull;
}
public string GetDestDir(string title, string asin)
{
// 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 finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
return finalDir;
}
public bool IsFileTypeMatch(FileInfo fileInfo)
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
#endregion
}
public class AudioFileStorage : AudibleFileStorage
{
public const string SKIP_FILE_EXT = "libhack";
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public AudioFileStorage() : base(FileType.Audio) { }
public string CreateSkipFile(string title, string asin, string contents = null)
{
var destinationDir = GetDestDir(title, asin);
Directory.CreateDirectory(destinationDir);
var path = FileUtility.GetValidFilename(destinationDir, title, SKIP_FILE_EXT, asin);
File.WriteAllText(path, contents ?? string.Empty);
return path;
}
}
public class AaxcFileStorage : AudibleFileStorage
{
protected override string[] Extensions { get; } = new[] { "aaxc" };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => DownloadsInProgress;
public AaxcFileStorage() : base(FileType.AAXC) { }
}
public class PdfFileStorage : AudibleFileStorage
{
protected override string[] Extensions { get; } = new[] { "pdf", "zip" };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public PdfFileStorage() : base(FileType.PDF) { }
}
}

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Octokit" Version="0.50.0" />
<PackageReference Include="Polly" Version="7.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core.Collections.Immutable;
using Newtonsoft.Json;
namespace FileManager
{
public static class FilePathCache
{
internal class CacheEntry
{
public string Id { get; set; }
public FileType FileType { get; set; }
public string Path { get; set; }
}
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
static FilePathCache()
{
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
if (File.Exists(jsonFile))
{
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(jsonFile));
cache = new Cache<CacheEntry>(list);
}
}
public static bool Exists(string id, FileType type) => GetPath(id, type) != null;
public static string GetPath(string id, FileType type)
{
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
if (entry == null)
return null;
if (!File.Exists(entry.Path))
{
remove(entry);
return null;
}
return entry.Path;
}
private static void remove(CacheEntry entry)
{
cache.Remove(entry);
save();
}
public static void Upsert(string id, FileType type, string path)
{
if (!File.Exists(path))
throw new FileNotFoundException("Cannot add path to cache. File not found");
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
if (entry is null)
cache.Add(new CacheEntry { Id = id, FileType = type, Path = path });
else
entry.Path = path;
save();
}
// cache is thread-safe and lock free. but file saving is not
private static object locker { get; } = new object();
private static void save()
{
// create json if not exists
static void resave() => File.WriteAllText(jsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented));
lock (locker)
{
try { resave(); }
catch (IOException)
{
try { resave(); }
catch (IOException ex)
{
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json");
throw;
}
}
}
}
}
}

View File

@@ -1,52 +0,0 @@
using System;
using System.IO;
namespace FileManager
{
public static class FileUtility
{
public static string GetValidFilename(string dirFullPath, string filename, string extension, params string[] metadataSuffixes)
{
if (string.IsNullOrWhiteSpace(dirFullPath))
throw new ArgumentException($"{nameof(dirFullPath)} may not be null or whitespace", nameof(dirFullPath));
// file max length = 255. dir max len = 247
// sanitize
filename = GetAsciiTag(filename);
// manage length
if (filename.Length > 50)
filename = filename.Substring(0, 50) + "[...]";
// append id. it is 10 or 14 char in the common cases
if (metadataSuffixes != null && metadataSuffixes.Length > 0)
filename += " [" + string.Join("][", metadataSuffixes) + "]";
// this method may also be used for directory names, so no guarantee of extension
if (!string.IsNullOrWhiteSpace(extension))
extension = '.' + extension.Trim('.');
// ensure uniqueness
var fullfilename = Path.Combine(dirFullPath, filename + extension);
var i = 0;
while (File.Exists(fullfilename))
fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
return fullfilename;
}
public static string GetAsciiTag(string property)
{
if (property == null)
return "";
// omit characters which are invalid. EXCEPTION: change colon to underscore
property = property.Replace(':', '_');
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
foreach (var ch in Path.GetInvalidFileNameChars())
property = property.Replace(ch.ToString(), "");
return property;
}
}
}

View File

@@ -1,113 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
namespace FileManager
{
public enum PictureSize { _80x80, _300x300, _500x500 }
public struct PictureDefinition
{
public string PictureId { get; }
public PictureSize Size { get; }
public PictureDefinition(string pictureId, PictureSize pictureSize)
{
PictureId = pictureId;
Size = pictureSize;
}
}
public static class PictureStorage
{
// not customizable. don't move to config
private static string ImagesDirectory { get; }
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("Images").FullName;
private static string getPath(PictureDefinition def)
=> Path.Combine(ImagesDirectory, $"{def.PictureId}{def.Size}.jpg");
private static System.Timers.Timer timer { get; }
static PictureStorage()
{
timer = new System.Timers.Timer(700)
{
AutoReset = true,
Enabled = true
};
timer.Elapsed += (_, __) => timerDownload();
}
public static event EventHandler<string> PictureCached;
private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>();
public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def)
{
if (!cache.ContainsKey(def))
{
var path = getPath(def);
cache[def]
= File.Exists(path)
? File.ReadAllBytes(path)
: null;
}
return (cache[def] == null, cache[def] ?? getDefaultImage(def.Size));
}
private static Dictionary<PictureSize, byte[]> defaultImages { get; } = new Dictionary<PictureSize, byte[]>();
public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes)
=> defaultImages[pictureSize] = bytes;
private static byte[] getDefaultImage(PictureSize size)
=> defaultImages.ContainsKey(size)
? defaultImages[size]
: new byte[0];
// necessary to avoid IO errors. ReadAllBytes and WriteAllBytes can conflict in some cases, esp when debugging
private static bool isProcessing;
private static void timerDownload()
{
// must live outside try-catch, else 'finally' can reset another thread's lock
if (isProcessing)
return;
try
{
isProcessing = true;
var def = cache
.Where(kvp => kvp.Value is null)
.Select(kvp => kvp.Key)
// 80x80 should be 1st since it's enum value == 0
.OrderBy(d => d.PictureId)
.FirstOrDefault();
// no more null entries. all requsted images are cached
if (string.IsNullOrWhiteSpace(def.PictureId))
return;
var bytes = downloadBytes(def);
saveFile(def, bytes);
cache[def] = bytes;
PictureCached?.Invoke(nameof(PictureStorage), def.PictureId);
}
finally
{
isProcessing = false;
}
}
private static HttpClient imageDownloadClient { get; } = new HttpClient();
private static byte[] downloadBytes(PictureDefinition def)
{
var sz = def.Size.ToString().Split('x')[1];
return imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}._SL{sz}_.jpg").Result;
}
private static void saveFile(PictureDefinition def, byte[] bytes)
{
var path = getPath(def);
File.WriteAllBytes(path, bytes);
}
}
}

View File

@@ -1,104 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApiDTOs;
using Dinah.Core;
using Polly;
using Polly.Retry;
namespace InternalUtilities
{
public static class AudibleApiActions
{
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public static Task<Api> GetApiAsync(string username, string localeName, ILoginCallback loginCallback = null)
{
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
{
Username = username.ToMask(),
LocaleName = localeName,
});
return EzApiCreator.GetApiAsync(
Localization.Get(localeName),
AudibleApiStorage.AccountsSettingsFile,
AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName),
loginCallback);
}
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public static Task<Api> GetApiAsync(ILoginCallback loginCallback, Account account)
{
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
{
Account = account?.MaskedLogEntry ?? "[null]",
LocaleName = account?.Locale?.Name
});
return EzApiCreator.GetApiAsync(
account.Locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath(),
loginCallback);
}
private static AsyncRetryPolicy policy { get; }
= Policy.Handle<Exception>()
// 2 retries == 3 total
.RetryAsync(2);
public static Task<List<Item>> GetLibraryValidatedAsync(Api api)
{
// 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 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(api));
}
private static async Task<List<Item>> getItemsAsync(Api api)
{
var items = await api.GetAllLibraryItemsAsync();
// remove episode parents
items.RemoveAll(i => i.IsEpisodes);
#region // episode handling. doesn't quite work
// // add individual/children episodes
// var childIds = items
// .Where(i => i.Episodes)
// .SelectMany(ep => ep.Relationships)
// .Where(r => r.RelationshipToProduct == AudibleApiDTOs.RelationshipToProduct.Child && r.RelationshipType == AudibleApiDTOs.RelationshipType.Episode)
// .Select(c => c.Asin)
// .ToList();
// foreach (var childId in childIds)
// {
// var bookResult = await api.GetLibraryBookAsync(childId, AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
// var bookItem = AudibleApiDTOs.LibraryDtoV10.FromJson(bookResult.ToString()).Item;
// items.Add(bookItem);
// }
#endregion
var validators = new List<IValidator>();
validators.AddRange(getValidators());
foreach (var v in validators)
{
var exceptions = v.Validate(items);
if (exceptions != null && exceptions.Any())
throw new AggregateException(exceptions);
}
return items;
}
private static List<IValidator> getValidators()
{
var type = typeof(IValidator);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && !p.IsInterface);
return types.Select(t => Activator.CreateInstance(t) as IValidator).ToList();
}
}
}

View File

@@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,256 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28803.156
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
ProjectSection(SolutionItems) = preProject
__TODO.txt = __TODO.txt
_DB_NOTES.txt = _DB_NOTES.txt
REFERENCE.txt = REFERENCE.txt
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5 Domain Utilities (db aware)", "5 Domain Utilities (db aware)", "{41CDCC73-9B81-49DD-9570-C54406E852AF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "6 Application", "6 Application", "{8679CAC8-9164-4007-BDD2-F004810EDA14}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1 Core Libraries", "1 Core Libraries", "{43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 Domain (db)", "4 Domain (db)", "{751093DD-5DBA-463E-ADBE-E05FAFB6983E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2 Utilities (domain ignorant)", "2 Utilities (domain ignorant)", "{7FBBB086-0807-4998-85BF-6D1A49C8AD05}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AaxDecrypter", "AaxDecrypter\AaxDecrypter.csproj", "{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager", "FileManager\FileManager.csproj", "{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataLayer", "DataLayer\DataLayer.csproj", "{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator", "FileLiberator\FileLiberator.csproj", "{393B5B27-D15C-4F77-9457-FA14BA8F3C73}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities", "InternalUtilities\InternalUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities (db ignorant)", "3 Domain Internal Utilities (db ignorant)", "{F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "LibationSearchEngine\LibationSearchEngine.csproj", "{2E1F5DB4-40CC-4804-A893-5DCE0193E598}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Tests", "0 Tests", "{38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Demos and Examples", "0 Demos and Examples", "{F61184E7-2426-4A13-ACEF-5689928E2CE2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Demos", "..\Dinah.Core\_Demos\Dinah.Core.Demos\Dinah.Core.Demos.csproj", "{9F1AA3DE-962F-469B-82B2-46F93491389B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Tests", "..\Dinah.Core\_Tests\Dinah.Core.Tests\Dinah.Core.Tests.csproj", "{E874D000-AD3A-4629-AC65-7219C2C7C1F0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCommon", "..\Dinah.Core\_Tests\TestCommon\TestCommon.csproj", "{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitAllRepos", "..\GitAllRepos\GitAllRepos\GitAllRepos.csproj", "{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi", "..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj", "{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi.Tests", "..\audible api\AudibleApi\_Tests\AudibleApi.Tests\AudibleApi.Tests.csproj", "{111420E2-D4F0-4068-B46A-C4B6DCC823DC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "LibationWinForms\LibationWinForms.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsDesigner", "WinFormsDesigner\WinFormsDesigner.csproj", "{0807616A-A77A-4B08-A65A-1582B09E114B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core", "..\Dinah.Core\Dinah.Core\Dinah.Core.csproj", "{9E951521-2587-4FC6-AD26-FAA9179FB6C4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore", "..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj", "{1255D9BA-CE6E-42E4-A253-6376540B9661}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2", "..\LuceneNet303r2\LuceneNet303r2\LuceneNet303r2.csproj", "{35803735-B669-4090-9681-CC7F7FABDC71}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2.Tests", "..\LuceneNet303r2\LuceneNet303r2.Tests\LuceneNet303r2.Tests.csproj", "{5A7681A5-60D9-480B-9AC7-63E0812A2548}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoImporterService\DtoImporterService.csproj", "{401865F5-1942-4713-B230-04544C0A97B0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs", "..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj", "{C03C5D65-3B7F-453B-972F-23950B7E0604}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs.Tests", "..\audible api\AudibleApi\_Tests\AudibleApiDTOs.Tests\AudibleApiDTOs.Tests.csproj", "{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiClientExample", "..\audible api\AudibleApi\_Demos\AudibleApiClientExample\AudibleApiClientExample.csproj", "{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop", "..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj", "{059CE32C-9AD6-45E9-A166-790DFFB0B730}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsDesktopUtilities", "WindowsDesktopUtilities\WindowsDesktopUtilities.csproj", "{E7EFD64D-6630-4426-B09C-B6862A92E3FD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libation Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests", "_Tests\LibationSearchEngine.Tests\LibationSearchEngine.Tests.csproj", "{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.Tests", "..\Dinah.Core\_Tests\Dinah.EntityFrameworkCore.Tests\Dinah.EntityFrameworkCore.Tests.csproj", "{6F5131A0-09AE-4707-B82B-5E53CB74688E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Release|Any CPU.Build.0 = Release|Any CPU
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Release|Any CPU.Build.0 = Release|Any CPU
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Release|Any CPU.Build.0 = Release|Any CPU
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Debug|Any CPU.Build.0 = Debug|Any CPU
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Release|Any CPU.ActiveCfg = Release|Any CPU
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Release|Any CPU.Build.0 = Release|Any CPU
{06882742-27A6-4347-97D9-56162CEC9C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06882742-27A6-4347-97D9-56162CEC9C11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06882742-27A6-4347-97D9-56162CEC9C11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06882742-27A6-4347-97D9-56162CEC9C11}.Release|Any CPU.Build.0 = Release|Any CPU
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.Build.0 = Release|Any CPU
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Release|Any CPU.Build.0 = Release|Any CPU
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Release|Any CPU.Build.0 = Release|Any CPU
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Release|Any CPU.Build.0 = Release|Any CPU
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Release|Any CPU.Build.0 = Release|Any CPU
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.Build.0 = Release|Any CPU
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.Build.0 = Release|Any CPU
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.Build.0 = Release|Any CPU
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.Build.0 = Release|Any CPU
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.Build.0 = Release|Any CPU
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.Build.0 = Release|Any CPU
{35803735-B669-4090-9681-CC7F7FABDC71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35803735-B669-4090-9681-CC7F7FABDC71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35803735-B669-4090-9681-CC7F7FABDC71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35803735-B669-4090-9681-CC7F7FABDC71}.Release|Any CPU.Build.0 = Release|Any CPU
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.Build.0 = Release|Any CPU
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.Build.0 = Release|Any CPU
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.Build.0 = Release|Any CPU
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.Build.0 = Release|Any CPU
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.Build.0 = Release|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.Build.0 = Debug|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.ActiveCfg = Release|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.Build.0 = Release|Any CPU
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Release|Any CPU.Build.0 = Release|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.Build.0 = Release|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.Build.0 = Release|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.Build.0 = Release|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.Build.0 = Release|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E}
{393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
{2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{9F1AA3DE-962F-469B-82B2-46F93491389B} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
{E874D000-AD3A-4629-AC65-7219C2C7C1F0} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{111420E2-D4F0-4068-B46A-C4B6DCC823DC} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{0807616A-A77A-4B08-A65A-1582B09E114B} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{9E951521-2587-4FC6-AD26-FAA9179FB6C4} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
{1255D9BA-CE6E-42E4-A253-6376540B9661} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
{35803735-B669-4090-9681-CC7F7FABDC71} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{5A7681A5-60D9-480B-9AC7-63E0812A2548} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{C03C5D65-3B7F-453B-972F-23950B7E0604} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
{E7EFD64D-6630-4426-B09C-B6862A92E3FD} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
{F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{6F5131A0-09AE-4707-B82B-5E53CB74688E} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{94BEB7CC-511D-45AB-9F09-09BE858EE486} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
EndGlobalSection
EndGlobal

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
<PublishTrimmed>true</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.4.4.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSBump" Version="2.3.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Octokit" Version="0.50.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LibationWinForms\LibationWinForms.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,491 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using AudibleApi;
using AudibleApi.Authorization;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Dinah.Core.IO;
using Dinah.Core.Logging;
using FileManager;
using InternalUtilities;
using LibationWinForms;
using LibationWinForms.Dialogs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
namespace LibationLauncher
{
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()
{
//// uncomment to see Console. MUST be called before anything writes to Console. Might only work from VS
//AllocConsole();
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// must occur before access to Configuration instance
migrate_to_v5_2_0__pre_config();
//***********************************************//
// //
// do not use Configuration before this line //
// //
//***********************************************//
var config = Configuration.Instance;
createSettings(config);
AudibleApiStorage.EnsureAccountsSettingsFileExists();
migrate_to_v5_0_0(config);
migrate_to_v5_2_0__post_config(config);
//migrate_to_v5_4_1(config);// comment out until after vacation
ensureSerilogConfig(config);
configureLogging(config);
logStartupState(config);
checkForUpdate(config);
Application.Run(new Form1());
}
private static void createSettings(Configuration config)
{
// all returns should be preceded by either:
// - if config.LibationSettingsAreValid
// - error message, Exit()
static void CancelInstallation()
{
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Application.Exit();
Environment.Exit(0);
}
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;
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
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 (libationFilesDialog.ShowDialog() != 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;
config.AllowLibationFixup = true;
config.DecryptToLossy = false;
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
{
CancelInstallation();
return;
}
if (config.LibationSettingsAreValid)
return;
CancelInstallation();
}
#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 (!config.Exists(nameof(config.AllowLibationFixup)))
config.AllowLibationFixup = true;
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();
var api = AudibleApiActions.GetApiAsync(new LibationWinForms.Login.WinformResponder(account), account).GetAwaiter().GetResult();
}
catch
{
// Don't care if it fails
}
}
}
#endregion
#region migrate to v5.2.0
// get rid of meta-directories, combine DownloadsInProgressEnum and DecryptInProgressEnum => InProgress
private static void migrate_to_v5_2_0__pre_config()
{
{
var settingsKey = "DownloadsInProgressEnum";
if (UNSAFE_MigrationHelper.Settings_TryGet(settingsKey, out var value))
{
UNSAFE_MigrationHelper.Settings_Delete(settingsKey);
UNSAFE_MigrationHelper.Settings_Insert("InProgress", translatePath(value));
}
}
{
UNSAFE_MigrationHelper.Settings_Delete("DecryptInProgressEnum");
}
{ // appsettings.json
var appSettingsKey = UNSAFE_MigrationHelper.LIBATION_FILES_KEY;
if (UNSAFE_MigrationHelper.APPSETTINGS_TryGet(appSettingsKey, out var value))
UNSAFE_MigrationHelper.APPSETTINGS_Update(appSettingsKey, translatePath(value));
}
}
private static string translatePath(string path)
=> path switch
{
"AppDir" => @".\LibationFiles",
"MyDocs" => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "LibationFiles")),
"UserProfile" => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation")),
"WinTemp" => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")),
_ => path
};
private 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
#region migrate to v5.4.1 see comment
// this 'migration' is a bit different. it intentionally runs each time Libation is started. its job will be fulfilled when I eventually
// implement the portion which removes FilePaths.json, at which time this method will be a proper migration
//
// I'm iterating through safe steps toward getting rid of the live scanner except to track audiobook files as a convenience
// such as clicking the stop light to open its location. live scanning will be replaced with state tracking in the database.
// FilePaths.json => db. long running. fire and forget
private static void migrate_to_v5_4_1(Configuration config)
=> new System.Threading.Thread(() => migrate_to_v5_4_1_thread(config)) { IsBackground = true }.Start();
private static void migrate_to_v5_4_1_thread(Configuration config)
{
var debugStopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
if (!File.Exists(filePaths))
return;
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.PdfStatus = LiberatedStatus.Liberated;
if (fileType == FileType.Audio)
{
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
book.UserDefinedItem.BookLocation = path;
}
}
context.SaveChanges();
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
}
debugStopwatch.Stop();
var debugTotal = debugStopwatch.Elapsed;
}
#endregion
private static void ensureSerilogConfig(Configuration config)
{
if (config.GetObject("Serilog") != 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", "File" },
{ "Args",
new JObject
{
// for this sink to work, a path must be provided. we override this below
{ "path", Path.Combine(config.LibationFiles, "_Log.log") },
{ "rollingInterval", "Month" },
// Serilog template formatting examples
// - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
// 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}" }
}
}
}
}
},
{ "Using", new JArray{ "Dinah.Core" } }, // dll's name, NOT namespace
{ "Enrich", new JArray{ "WithCaller" } },
};
config.SetObject("Serilog", serilogObj);
}
// to restore original: Console.SetOut(origOut);
private static TextWriter origOut { get; } = Console.Out;
private static void configureLogging(Configuration config)
{
config.ConfigureLogging();
// Fwd Console to serilog.
// Serilog also write to Console (should probably change this) so it might be asking for trouble.
// SerilogTextWriter needs to be more robust and tested. Esp the Write() methods.
// Empirical testing so far has shown no issues.
Console.SetOut(new MultiTextWriter(origOut, new SerilogTextWriter()));
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
}
private static void logStartupState(Configuration config)
{
// begin logging session with a form feed
Log.Logger.Information("\r\n\f");
Log.Logger.Information("Begin Libation. {@DebugInfo}", new
{
Version = BuildVersion.ToString(),
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
config.LibationFiles,
AudibleFileStorage.BooksDirectory,
config.InProgress,
DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
});
MessageBoxVerboseLoggingWarning.ShowIfTrue();
}
private static void checkForUpdate(Configuration config)
{
string zipUrl;
string selectedPath;
try
{
// timed out
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
if (latest is null)
return;
var latestVersionString = latest.TagName.Trim('v');
if (!Version.TryParse(latestVersionString, out var latestRelease))
return;
// we're up to date
if (latestRelease <= BuildVersion)
return;
// we have an update
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
zipUrl = zip?.BrowserDownloadUrl;
Log.Logger.Information("Update available: {@DebugInfo}", new {
latestRelease = latestRelease.ToString(),
latest.HtmlUrl,
zipUrl
});
if (zipUrl is null)
{
MessageBox.Show(latest.HtmlUrl, "New version available");
return;
}
var result = MessageBox.Show($"New version available @ {latest.HtmlUrl}\r\nDownload the zip file?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result != DialogResult.Yes)
return;
using var fileSelector = new SaveFileDialog { FileName = zip.Name, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
if (fileSelector.ShowDialog() != DialogResult.OK)
return;
selectedPath = fileSelector.FileName;
}
catch (AggregateException aggEx)
{
Log.Logger.Error(aggEx, "Checking for new version too often");
return;
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show("Error checking for update", "Error checking for update", ex);
return;
}
try
{
LibationWinForms.BookLiberation.ProcessorAutomationController.DownloadFile(zipUrl, selectedPath, true);
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show("Error downloading update", "Error downloading update", ex);
}
}
private static Octokit.Release getLatestRelease(TimeSpan timeout)
{
var task = System.Threading.Tasks.Task.Run(() => getLatestRelease());
if (task.Wait(timeout))
return task.Result;
Log.Logger.Information("Timed out");
return null;
}
private static Octokit.Release getLatestRelease()
{
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue("Libation"));
// https://octokitnet.readthedocs.io/en/latest/releases/
var releases = gitHubClient.Repository.Release.GetAll("rmcrackan", "Libation").GetAwaiter().GetResult();
var latest = releases.First(r => !r.Draft && !r.Prerelease);
return latest;
}
private static Version BuildVersion => System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
}
}

View File

@@ -1,34 +0,0 @@
using System;
using System.Windows.Forms;
using Dinah.Core.Windows.Forms;
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.UIThread(() => 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,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,54 +0,0 @@
using System;
using System.Windows.Forms;
using Dinah.Core.Windows.Forms;
namespace LibationWinForms.BookLiberation
{
public partial class DecryptForm : Form
{
public DecryptForm() => InitializeComponent();
// book info
private string title;
private string authorNames;
private string narratorNames;
public void SetTitle(string actionName, string title)
{
this.UIThread(() => this.Text = actionName + " " + title);
this.title = title;
updateBookInfo();
}
public void SetAuthorNames(string authorNames)
{
this.authorNames = authorNames;
updateBookInfo();
}
public void SetNarratorNames(string narratorNames)
{
this.narratorNames = narratorNames;
updateBookInfo();
}
// thread-safe UI updates
private void updateBookInfo()
=> bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
public void SetCoverImage(System.Drawing.Image coverImage)
=> pictureBox1.UIThread(() => pictureBox1.Image = coverImage);
public void UpdateProgress(int percentage)
{
if (percentage == 0)
updateRemainingTime(0);
else
progressBar1.UIThread(() => progressBar1.Value = percentage);
}
public void UpdateRemainingTime(TimeSpan remaining)
=> updateRemainingTime((int)remaining.TotalSeconds);
private void updateRemainingTime(int remaining)
=> remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec");
}
}

View File

@@ -1,59 +0,0 @@
using System;
using System.Windows.Forms;
using Dinah.Core.Windows.Forms;
namespace LibationWinForms.BookLiberation
{
public partial class DownloadForm : Form
{
public DownloadForm()
{
InitializeComponent();
progressLbl.Text = "";
filenameLbl.Text = "";
}
// thread-safe UI updates
public void UpdateFilename(string title) => filenameLbl.UIThread(() => filenameLbl.Text = title);
public void DownloadProgressChanged(long BytesReceived, long? TotalBytesToReceive)
{
// this won't happen with download file. it will happen with download string
if (!TotalBytesToReceive.HasValue || TotalBytesToReceive.Value <= 0)
return;
progressLbl.UIThread(() => progressLbl.Text = $"{BytesReceived:#,##0} of {TotalBytesToReceive.Value:#,##0}");
var d = double.Parse(BytesReceived.ToString()) / double.Parse(TotalBytesToReceive.Value.ToString()) * 100.0;
var i = int.Parse(Math.Truncate(d).ToString());
progressBar1.UIThread(() => progressBar1.Value = i);
lastDownloadProgress = DateTime.Now;
}
#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.UIThread(() => 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.UIThread(() => 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,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,567 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using Dinah.Core.Windows.Forms;
using FileLiberator;
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(string productId, EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupSingleBookAsync) + " {@DebugInfo}", new { productId });
var backupBook = getWiredUpBackupBook(completedAction);
(Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook);
var libraryBook = IProcessableExt.GetSingleLibraryBook(productId);
// continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
unsubscribeEvents();
}
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var backupBook = getWiredUpBackupBook(completedAction);
var automatedBackupsForm = new AutomatedBackupsForm();
(Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook, automatedBackupsForm);
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
unsubscribeEvents();
}
public static async Task ConvertAllBooksAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync));
var convertBook = new ConvertToMp3();
convertBook.Begin += (_, l) => wireUpEvents(convertBook, l, "Converting");
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
void statusUpdate(object _, string str) => logMe.Info("- " + str);
void convertBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Convert Step, Begin: {libraryBook.Book}");
void convertBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Convert Step, Completed: {libraryBook.Book}{Environment.NewLine}");
convertBook.Begin += convertBookBegin;
convertBook.StatusUpdate += statusUpdate;
convertBook.Completed += convertBookCompleted;
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
convertBook.Begin -= convertBookBegin;
convertBook.StatusUpdate -= statusUpdate;
convertBook.Completed -= convertBookCompleted;
}
private static BackupBook getWiredUpBackupBook(EventHandler<LibraryBook> completedAction)
{
var backupBook = new BackupBook();
backupBook.DownloadDecryptBook.Begin += (_, l) => wireUpEvents(backupBook.DownloadDecryptBook, l);
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
if (completedAction != null)
{
backupBook.DownloadDecryptBook.Completed += completedAction;
backupBook.DownloadPdf.Completed += completedAction;
}
return backupBook;
}
private static (Action unsubscribeEvents, LogMe) attachToBackupsForm(BackupBook backupBook, AutomatedBackupsForm automatedBackupsForm = null)
{
#region create logger
var logMe = LogMe.RegisterForm(automatedBackupsForm);
#endregion
#region define how model actions will affect form behavior
void statusUpdate(object _, string str) => logMe.Info("- " + str);
void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished
void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
void downloadPdfBegin(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished
void downloadPdfCompleted(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}");
#endregion
#region subscribe new form to model's events
backupBook.DownloadDecryptBook.Begin += decryptBookBegin;
backupBook.DownloadDecryptBook.StatusUpdate += statusUpdate;
backupBook.DownloadDecryptBook.Completed += decryptBookCompleted;
backupBook.DownloadPdf.Begin += downloadPdfBegin;
backupBook.DownloadPdf.StatusUpdate += statusUpdate;
backupBook.DownloadPdf.Completed += downloadPdfCompleted;
#endregion
#region when form closes, unsubscribe from model's events
// unsubscribe so disposed forms aren't still trying to receive notifications
Action unsubscribe = () =>
{
backupBook.DownloadDecryptBook.Begin -= decryptBookBegin;
backupBook.DownloadDecryptBook.StatusUpdate -= statusUpdate;
backupBook.DownloadDecryptBook.Completed -= decryptBookCompleted;
backupBook.DownloadPdf.Begin -= downloadPdfBegin;
backupBook.DownloadPdf.StatusUpdate -= statusUpdate;
backupBook.DownloadPdf.Completed -= downloadPdfCompleted;
};
#endregion
return (unsubscribe, logMe);
}
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var downloadPdf = getWiredUpDownloadPdf(completedAction);
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
}
private static DownloadPdf getWiredUpDownloadPdf(EventHandler<LibraryBook> completedAction)
{
var downloadPdf = new DownloadPdf();
downloadPdf.Begin += (_, __) => wireUpEvents(downloadPdf);
if (completedAction != null)
downloadPdf.Completed += completedAction;
return downloadPdf;
}
public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false)
{
var downloadDialog = new DownloadForm();
downloadDialog.UpdateFilename(destination);
downloadDialog.Show();
new System.Threading.Thread(() =>
{
var downloadFile = new DownloadFile();
downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.UIThread(() =>
downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive)
);
downloadFile.DownloadCompleted += (_, __) => downloadDialog.UIThread(() =>
{
downloadDialog.Close();
if (showDownloadCompletedDialog)
MessageBox.Show("File downloaded");
});
downloadFile.PerformDownloadFileAsync(url, destination).GetAwaiter().GetResult();
})
{ IsBackground = true }
.Start();
}
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
private static void wireUpEvents(IDownloadableProcessable downloadable)
{
#region create form
var downloadDialog = new DownloadForm();
#endregion
// extra complexity for wiring up download form:
// case 1: download is needed
// dialog created. subscribe to events
// downloadable.DownloadBegin fires. shows dialog
// downloadable.DownloadCompleted fires. closes dialog. which fires FormClosing, FormClosed, Disposed
// Disposed unsubscribe from events
// case 2: download is not needed
// dialog created. subscribe to events
// dialog is never shown nor closed
// downloadable.Completed fires. disposes dialog and unsubscribes from events
#region define how model actions will affect form behavior
void downloadBegin(object _, string str)
{
downloadDialog.UpdateFilename(str);
downloadDialog.Show();
}
// close form on DOWNLOAD completed, not final Completed. Else for BackupBook this form won't close until DECRYPT is also complete
void fileDownloadCompleted(object _, string __) => downloadDialog.Close();
void downloadProgressChanged(object _, Dinah.Core.Net.Http.DownloadProgress progress)
=> downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive);
void unsubscribe(object _ = null, EventArgs __ = null)
{
downloadable.DownloadBegin -= downloadBegin;
downloadable.DownloadCompleted -= fileDownloadCompleted;
downloadable.DownloadProgressChanged -= downloadProgressChanged;
downloadable.Completed -= dialogDispose;
}
// unless we dispose, if the form is created but un-used/never-shown then weird UI stuff can happen
// also, since event unsubscribe occurs on FormClosing and an unused form is never closed, then the events will never be unsubscribed
void dialogDispose(object _, object __)
{
if (!downloadDialog.IsDisposed)
downloadDialog.Dispose();
}
#endregion
#region subscribe new form to model's events
downloadable.DownloadBegin += downloadBegin;
downloadable.DownloadCompleted += fileDownloadCompleted;
downloadable.DownloadProgressChanged += downloadProgressChanged;
downloadable.Completed += dialogDispose;
#endregion
#region when form closes, unsubscribe from model's events
// unsubscribe so disposed forms aren't still trying to receive notifications
// FormClosing is more UI safe but won't fire unless the form is shown and closed
// if form was shown, Disposed will fire for FormClosing, FormClosed, and Disposed
// if not shown, it will still fire for Disposed
downloadDialog.Disposed += unsubscribe;
#endregion
}
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
private static void wireUpEvents(IDecryptable decryptBook, LibraryBook libraryBook, string actionName = "Decrypting")
{
#region create form
var decryptDialog = new DecryptForm();
#endregion
#region Set initially displayed book properties from library info.
decryptDialog.SetTitle(actionName, libraryBook.Book.Title);
decryptDialog.SetAuthorNames(string.Join(", ", libraryBook.Book.Authors));
decryptDialog.SetNarratorNames(string.Join(", ", libraryBook.Book.NarratorNames));
decryptDialog.SetCoverImage(
WindowsDesktopUtilities.WinAudibleImageServer.GetImage(
libraryBook.Book.PictureId,
FileManager.PictureSize._80x80
));
#endregion
#region define how model actions will affect form behavior
void decryptBegin(object _, string __) => decryptDialog.Show();
void titleDiscovered(object _, string title) => decryptDialog.SetTitle(actionName, title);
void authorsDiscovered(object _, string authors) => decryptDialog.SetAuthorNames(authors);
void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators);
void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(Dinah.Core.Drawing.ImageReader.ToImage(coverBytes));
void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage);
void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining);
void decryptCompleted(object _, string __) => decryptDialog.Close();
void requestCoverArt(object _, Action<byte[]> setCoverArtDelegate)
{
var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500);
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(picDef);
if (isDefault)
{
void pictureCached(object _, string pictureId)
{
if (pictureId == libraryBook.Book.PictureId)
{
FileManager.PictureStorage.PictureCached -= pictureCached;
var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500);
(_, picture) = FileManager.PictureStorage.GetPicture(picDef);
setCoverArtDelegate(picture);
}
};
FileManager.PictureStorage.PictureCached += pictureCached;
}
else
setCoverArtDelegate(picture);
}
#endregion
#region subscribe new form to model's events
decryptBook.DecryptBegin += decryptBegin;
decryptBook.TitleDiscovered += titleDiscovered;
decryptBook.AuthorsDiscovered += authorsDiscovered;
decryptBook.NarratorsDiscovered += narratorsDiscovered;
decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered;
decryptBook.UpdateProgress += updateProgress;
decryptBook.UpdateRemainingTime += updateRemainingTime;
decryptBook.RequestCoverArt += requestCoverArt;
decryptBook.DecryptCompleted += decryptCompleted;
#endregion
#region when form closes, unsubscribe from model's events
// unsubscribe so disposed forms aren't still trying to receive notifications
decryptDialog.FormClosing += (_, __) =>
{
decryptBook.DecryptBegin -= decryptBegin;
decryptBook.TitleDiscovered -= titleDiscovered;
decryptBook.AuthorsDiscovered -= authorsDiscovered;
decryptBook.NarratorsDiscovered -= narratorsDiscovered;
decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered;
decryptBook.UpdateProgress -= updateProgress;
decryptBook.UpdateRemainingTime -= updateRemainingTime;
decryptBook.RequestCoverArt -= requestCoverArt;
decryptBook.DecryptCompleted -= decryptCompleted;
decryptBook.Cancel();
};
#endregion
}
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable)
{
#region create form and logger
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
#endregion
#region define how model actions will affect form behavior
void begin(object _, LibraryBook libraryBook) => logMe.Info($"Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => logMe.Info("- " + str);
// extra line after book is completely finished
void completed(object _, LibraryBook libraryBook) => logMe.Info($"Completed: {libraryBook.Book}{Environment.NewLine}");
#endregion
#region subscribe new form to model's events
downloadable.Begin += begin;
downloadable.StatusUpdate += statusUpdate;
downloadable.Completed += completed;
#endregion
#region when form closes, unsubscribe from model's events
// unsubscribe so disposed forms aren't still trying to receive notifications
automatedBackupsForm.FormClosing += (_, __) =>
{
downloadable.Begin -= begin;
downloadable.StatusUpdate -= statusUpdate;
downloadable.Completed -= completed;
};
#endregion
return (automatedBackupsForm, logMe);
}
}
abstract class BackupRunner
{
protected LogMe LogMe { get; }
protected IProcessable Processable { get; }
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
protected BackupRunner(LogMe logMe, IProcessable 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 DialogResult CreateSkipFileResult { 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(Func<LibraryBook, Task<StatusHandler>> func, LibraryBook libraryBook)
{
string logMessage;
try
{
var statusHandler = await func(libraryBook);
if (statusHandler.IsSuccess)
return true;
foreach (var errorMessage in statusHandler.Errors)
LogMe.Error(errorMessage);
logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}");
}
catch (Exception ex)
{
LogMe.Error(ex);
logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace;
}
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
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]";
}
var dialogResult = MessageBox.Show(string.Format(SkipDialogText, details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question);
if (dialogResult == DialogResult.Abort)
return false;
if (dialogResult == CreateSkipFileResult)
{
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error, null);
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
LogMe.Info($@"
Created new 'skip' file
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
{path}
".Trim());
}
return true;
}
}
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 DialogResult CreateSkipFileResult => DialogResult.Yes;
public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook)
: base(logMe, processable)
{
_libraryBook = libraryBook;
}
protected override async Task RunAsync()
{
if (_libraryBook is not null)
await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook);
}
}
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 DialogResult CreateSkipFileResult => DialogResult.Ignore;
public BackupLoop(LogMe logMe, IProcessable 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())
{
var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook);
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,101 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
using FileManager;
namespace LibationWinForms.Dialogs
{
public partial class EditQuickFilters : Form
{
const string BLACK_UP_POINTING_TRIANGLE = "\u25B2";
const string BLACK_DOWN_POINTING_TRIANGLE = "\u25BC";
const string COL_Original = nameof(Original);
const string COL_Delete = nameof(Delete);
const string COL_Filter = nameof(Filter);
const string COL_MoveUp = nameof(MoveUp);
const string COL_MoveDown = nameof(MoveDown);
Form1 _parent { get; }
public EditQuickFilters(Form1 parent)
{
_parent = parent;
InitializeComponent();
dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
populateGridValues();
}
private void populateGridValues()
{
var filters = QuickFilters.Filters;
if (!filters.Any())
return;
foreach (var filter in filters)
dataGridView1.Rows.Add(filter, "X", filter, BLACK_UP_POINTING_TRIANGLE, BLACK_DOWN_POINTING_TRIANGLE);
}
private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e)
{
e.Row.Cells[COL_Delete].Value = "X";
e.Row.Cells[COL_MoveUp].Value = BLACK_UP_POINTING_TRIANGLE;
e.Row.Cells[COL_MoveDown].Value = BLACK_DOWN_POINTING_TRIANGLE;
}
private void saveBtn_Click(object sender, EventArgs e)
{
var list = dataGridView1.Rows
.OfType<DataGridViewRow>()
.Select(r => r.Cells[COL_Filter].Value?.ToString())
.ToList();
QuickFilters.ReplaceAll(list);
_parent.UpdateFilterDropDown();
this.DialogResult = DialogResult.OK;
this.Close();
}
private void cancelBtn_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
var dgv = (DataGridView)sender;
var col = dgv.Columns[e.ColumnIndex];
if (col is DataGridViewButtonColumn && e.RowIndex >= 0)
{
var row = dgv.Rows[e.RowIndex];
switch (col.Name)
{
case COL_Delete:
// if final/edit row: do nothing
if (e.RowIndex < dgv.RowCount - 1)
dgv.Rows.Remove(row);
break;
case COL_MoveUp:
// if top: do nothing
if (e.RowIndex < 1)
break;
dgv.Rows.Remove(row);
dgv.Rows.Insert(e.RowIndex - 1, row);
break;
case COL_MoveDown:
// if final/edit row or bottom filter row: do nothing
if (e.RowIndex >= dgv.RowCount - 2)
break;
dgv.Rows.Remove(row);
dgv.Rows.Insert(e.RowIndex + 1, row);
break;
}
}
}
}
}

View File

@@ -1,91 +0,0 @@
namespace LibationWinForms.Dialogs
{
partial class EditTagsDialog
{
/// <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.SaveBtn = new System.Windows.Forms.Button();
this.newTagsTb = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// SaveBtn
//
this.SaveBtn.Location = new System.Drawing.Point(396, 25);
this.SaveBtn.Name = "SaveBtn";
this.SaveBtn.Size = new System.Drawing.Size(75, 23);
this.SaveBtn.TabIndex = 1;
this.SaveBtn.Text = "Save";
this.SaveBtn.UseVisualStyleBackColor = true;
this.SaveBtn.Click += new System.EventHandler(this.SaveBtn_Click);
//
// newTagsTb
//
this.newTagsTb.Location = new System.Drawing.Point(12, 27);
this.newTagsTb.Name = "newTagsTb";
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.newTagsTb.Size = new System.Drawing.Size(375, 20);
this.newTagsTb.TabIndex = 0;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(424, 13);
this.label1.TabIndex = 2;
this.label1.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
"ores";
//
// EditTagsDialog
//
this.AcceptButton = this.SaveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(483, 60);
this.Controls.Add(this.label1);
this.Controls.Add(this.newTagsTb);
this.Controls.Add(this.SaveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "EditTagsDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Tags";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button SaveBtn;
private System.Windows.Forms.TextBox newTagsTb;
private System.Windows.Forms.Label label1;
}
}

View File

@@ -1,27 +0,0 @@
using System;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class EditTagsDialog : Form
{
public string NewTags { get; private set; }
public EditTagsDialog()
{
InitializeComponent();
}
public EditTagsDialog(string title, string rawTags) : this()
{
this.Text = $"Edit Tags - {title}";
this.newTagsTb.Text = rawTags;
}
private void SaveBtn_Click(object sender, EventArgs e)
{
NewTags = this.newTagsTb.Text;
DialogResult = DialogResult.OK;
}
}
}

View File

@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,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,48 +0,0 @@
using System;
using System.Windows.Forms;
using ApplicationServices;
using InternalUtilities;
using LibationWinForms.Login;
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) => new WinformResponder(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,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,20 +0,0 @@
using System;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class SearchSyntaxDialog : Form
{
public SearchSyntaxDialog()
{
InitializeComponent();
label2.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields());
label3.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields());
label4.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields());
label5.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields());
}
private void CloseBtn_Click(object sender, EventArgs e) => this.Close();
}
}

View File

@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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,251 +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.convertLossyRb = new System.Windows.Forms.RadioButton();
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
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.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 = 2;
this.booksLocationDescLbl.Text = "[book desc]";
//
// inProgressDescLbl
//
this.inProgressDescLbl.AutoSize = true;
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 127);
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 = 1;
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, 419);
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 = 4;
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, 419);
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 = 5;
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.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.advancedSettingsGb.Controls.Add(this.convertLossyRb);
this.advancedSettingsGb.Controls.Add(this.convertLosslessRb);
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
this.advancedSettingsGb.Controls.Add(this.allowLibationFixupCbox);
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, 232);
this.advancedSettingsGb.TabIndex = 5;
this.advancedSettingsGb.TabStop = false;
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
//
// convertLossyRb
//
this.convertLossyRb.AutoSize = true;
this.convertLossyRb.Location = new System.Drawing.Point(7, 88);
this.convertLossyRb.Name = "convertLossyRb";
this.convertLossyRb.Size = new System.Drawing.Size(242, 19);
this.convertLossyRb.TabIndex = 0;
this.convertLossyRb.Text = "Download my books as .MP3 files (Lossy)";
this.convertLossyRb.UseVisualStyleBackColor = true;
//
// convertLosslessRb
//
this.convertLosslessRb.AutoSize = true;
this.convertLosslessRb.Checked = true;
this.convertLosslessRb.Location = new System.Drawing.Point(7, 63);
this.convertLosslessRb.Name = "convertLosslessRb";
this.convertLosslessRb.Size = new System.Drawing.Size(327, 19);
this.convertLosslessRb.TabIndex = 0;
this.convertLosslessRb.TabStop = true;
this.convertLosslessRb.Text = "Download my books as .M4B files (Lossless Mp4a format)";
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(10, 175);
this.inProgressSelectControl.Name = "inProgressSelectControl";
this.inProgressSelectControl.Size = new System.Drawing.Size(552, 52);
this.inProgressSelectControl.TabIndex = 2;
//
// allowLibationFixupCbox
//
this.allowLibationFixupCbox.AutoSize = true;
this.allowLibationFixupCbox.Checked = true;
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
this.allowLibationFixupCbox.Location = new System.Drawing.Point(7, 22);
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
this.allowLibationFixupCbox.Size = new System.Drawing.Size(262, 19);
this.allowLibationFixupCbox.TabIndex = 0;
this.allowLibationFixupCbox.Text = "Allow Libation to fix up audiobook metadata";
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
//
// 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 = 4;
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.Name = "booksSelectControl";
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
this.booksSelectControl.TabIndex = 1;
//
// 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 = 1;
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 = 2;
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 = 3;
//
// 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, 462);
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.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;
}
}

View File

@@ -1,123 +0,0 @@
using System;
using System.IO;
using System.Windows.Forms;
using Dinah.Core;
using FileManager;
namespace LibationWinForms.Dialogs
{
public partial class SettingsDialog : Form
{
Configuration config { get; } = Configuration.Instance;
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.booksLocationDescLbl.Text = desc(nameof(config.Books));
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
booksSelectControl.SetSearchTitle("books location");
booksSelectControl.SetDirectoryItems(
new()
{
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs
},
Configuration.KnownDirectories.UserProfile,
"Books");
booksSelectControl.SelectDirectory(config.Books);
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
convertLosslessRb.Checked = !config.DecryptToLossy;
convertLossyRb.Checked = config.DecryptToLossy;
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);
}
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
{
convertLosslessRb.Enabled = allowLibationFixupCbox.Checked;
convertLossyRb.Enabled = allowLibationFixupCbox.Checked;
if (!allowLibationFixupCbox.Checked)
{
convertLosslessRb.Checked = true;
}
}
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.AllowLibationFixup = allowLibationFixupCbox.Checked;
config.DecryptToLossy = convertLossyRb.Checked;
config.InProgress = inProgressSelectControl.SelectedDirectory;
this.DialogResult = DialogResult.OK;
this.Close();
}
private void cancelBtn_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
}

View File

@@ -1,372 +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.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.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);
//
// 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(284, 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(284, 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(284, 22);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]";
this.convertAllM4bToMp3ToolStripMenuItem.Visible = false;
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(61, 17);
this.visibleCountLbl.Text = "Visible: {0}";
//
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(375, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
//
this.backupsCountsLbl.Name = "backupsCountsLbl";
this.backupsCountsLbl.Size = new System.Drawing.Size(336, 17);
this.backupsCountsLbl.Text = "BACKUPS: No progress: {0} Encrypted: {1} Fully backed up: {2}";
//
// pdfsCountsLbl
//
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.pdfsCountsLbl.Text = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
//
// 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.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
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;
}
}

View File

@@ -1,459 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.Drawing;
using Dinah.Core.Windows.Forms;
using FileManager;
using InternalUtilities;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
public partial class Form1 : Form
{
private string backupsCountsLbl_Format { get; }
private string pdfsCountsLbl_Format { get; }
private string visibleCountLbl_Format { get; }
private string beginBookBackupsToolStripMenuItem_format { get; }
private string beginPdfBackupsToolStripMenuItem_format { get; }
public Form1()
{
InitializeComponent();
// back up string formats
backupsCountsLbl_Format = backupsCountsLbl.Text;
pdfsCountsLbl_Format = pdfsCountsLbl.Text;
visibleCountLbl_Format = visibleCountLbl.Text;
beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text;
beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text;
// after backing up formats: can set default/temp visible text
backupsCountsLbl.Text = "[Calculating backed up book quantities]";
pdfsCountsLbl.Text = "[Calculating backed up PDFs]";
setVisibleCount(null, 0);
if (this.DesignMode)
return;
// independent UI updates
this.Load += setBackupCountsAsync;
this.Load += (_, __) => RestoreSizeAndLocation();
this.Load += (_, __) => RefreshImportMenu();
// start background service
this.Load += (_, __) => startBackgroundImageDownloader();
}
private static void startBackgroundImageDownloader()
{
// load default/missing cover images. this will also initiate the background image downloader
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;
reloadGrid();
// also applies filter. ONLY call AFTER loading grid
loadInitialQuickFilterState();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
SaveSizeAndLocation();
}
private void RestoreSizeAndLocation()
{
var config = Configuration.Instance;
var width = config.MainFormWidth;
var height = config.MainFormHeight;
// too small -- something must have gone wrong. use defaults
if (width < 25 || height < 25)
{
width = 1023;
height = 578;
}
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
if (width > Screen.PrimaryScreen.WorkingArea.Width)
width = Screen.PrimaryScreen.WorkingArea.Width;
if (height > Screen.PrimaryScreen.WorkingArea.Height)
height = Screen.PrimaryScreen.WorkingArea.Height;
var x = config.MainFormX;
var y = config.MainFormY;
var rect = new System.Drawing.Rectangle(x, y, width, height);
// is proposed rect on a screen?
if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect)))
{
this.StartPosition = FormStartPosition.Manual;
this.DesktopBounds = rect;
}
else
{
this.StartPosition = FormStartPosition.WindowsDefaultLocation;
this.Size = rect.Size;
}
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
this.WindowState = config.MainFormIsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
}
private void SaveSizeAndLocation()
{
System.Drawing.Point location;
System.Drawing.Size size;
// save location and size if the state is normal
if (this.WindowState == FormWindowState.Normal)
{
location = this.Location;
size = this.Size;
}
else
{
// save the RestoreBounds if the form is minimized or maximized
location = this.RestoreBounds.Location;
size = this.RestoreBounds.Size;
}
var config = Configuration.Instance;
config.MainFormX = location.X;
config.MainFormY = location.Y;
config.MainFormWidth = size.Width;
config.MainFormHeight = size.Height;
config.MainFormIsMaximized = this.WindowState == FormWindowState.Maximized;
}
#region reload grid
bool isProcessingGridSelect = false;
private void reloadGrid()
{
// suppressed filter while init'ing UI
var prev_isProcessingGridSelect = isProcessingGridSelect;
isProcessingGridSelect = true;
setGrid();
isProcessingGridSelect = prev_isProcessingGridSelect;
// UI init complete. now we can apply filter
doFilter(lastGoodFilter);
}
ProductsGrid currProductsGrid;
private void setGrid()
{
SuspendLayout();
{
if (currProductsGrid != null)
{
gridPanel.Controls.Remove(currProductsGrid);
currProductsGrid.VisibleCountChanged -= setVisibleCount;
currProductsGrid.BackupCountsChanged -= setBackupCountsAsync;
currProductsGrid.Dispose();
}
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
currProductsGrid.VisibleCountChanged += setVisibleCount;
currProductsGrid.BackupCountsChanged += setBackupCountsAsync;
gridPanel.UIThread(() => gridPanel.Controls.Add(currProductsGrid));
currProductsGrid.Display();
}
ResumeLayout();
}
#endregion
#region bottom: qty books visible
private void setVisibleCount(object _, int qty) => visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty);
#endregion
#region bottom: backup counts
private async void setBackupCountsAsync(object _, object __)
{
LibraryCommands.LibraryStats libraryStats = null;
await Task.Run(() => libraryStats = LibraryCommands.GetCounts());
setBookBackupCounts(libraryStats.booksFullyBackedUp, libraryStats.booksDownloadedOnly, libraryStats.booksNoProgress);
setPdfBackupCounts(libraryStats.pdfsDownloaded, libraryStats.pdfsNotDownloaded);
}
private void setBookBackupCounts(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress)
{
// enable/disable export
var hasResults = 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress);
exportLibraryToolStripMenuItem.Enabled = hasResults;
// update bottom numbers
var pending = booksNoProgress + booksDownloadedOnly;
var statusStripText
= !hasResults ? "No books. Begin by importing your library"
: pending > 0 ? string.Format(backupsCountsLbl_Format, booksNoProgress, booksDownloadedOnly, booksFullyBackedUp)
: $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up";
// update menu item
var menuItemText
= pending > 0
? $"{pending} remaining"
: "All books have been liberated";
// update UI
statusStrip1.UIThread(() => backupsCountsLbl.Text = statusStripText);
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
}
private void setPdfBackupCounts(int pdfsDownloaded, int pdfsNotDownloaded)
{
// update bottom numbers
var hasResults = 0 < (pdfsNotDownloaded + pdfsDownloaded);
var statusStripText
= !hasResults ? ""
: pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, pdfsNotDownloaded, pdfsDownloaded)
: $"| All {pdfsDownloaded} PDFs downloaded";
// update menu item
var menuItemText
= pdfsNotDownloaded > 0
? $"{pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
// update UI
statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText);
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = pdfsNotDownloaded > 0);
menuStrip1.UIThread(() => 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();
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()
{
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;
}
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 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}");
if (totalProcessed > 0)
reloadGrid();
}
#endregion
#region liberate menu
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow);
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow);
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
#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;
}
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,202 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using DataLayer;
namespace LibationWinForms
{
internal class GridEntry
{
private LibraryBook libraryBook { get; }
private Book book => libraryBook.Book;
public Book GetBook() => book;
public GridEntry(LibraryBook libraryBook) => this.libraryBook = libraryBook;
// hide from public fields from Data Source GUI with [Browsable(false)]
[Browsable(false)]
public string Tags => book.UserDefinedItem.Tags;
[Browsable(false)]
public IEnumerable<string> TagsEnumerated => book.UserDefinedItem.TagsEnumerated;
public enum LiberatedState { NotDownloaded, PartialDownload, Liberated }
[Browsable(false)]
public LiberatedState Liberated_Status
=> ApplicationServices.TransitionalFileLocator.Audio_Exists(book.AudibleProductId) ? LiberatedState.Liberated
: ApplicationServices.TransitionalFileLocator.AAXC_Exists(book.AudibleProductId) ? LiberatedState.PartialDownload
: LiberatedState.NotDownloaded;
public enum PdfState { NoPdf, Downloaded, NotDownloaded }
[Browsable(false)]
public PdfState Pdf_Status
=> !book.Supplements.Any() ? PdfState.NoPdf
: ApplicationServices.TransitionalFileLocator.PDF_Exists(book.AudibleProductId) ? PdfState.Downloaded
: PdfState.NotDownloaded;
// displayValues is what gets displayed
// the value that gets returned from the property is the cell's value
// this allows for the value to be sorted one way and displayed another
// eg:
// orig title: The Computer
// formatReplacement: The Computer
// value for sorting: Computer
private Dictionary<string, string> displayValues { get; } = new Dictionary<string, string>();
public bool TryDisplayValue(string key, out string value) => displayValues.TryGetValue(key, out value);
public Image Cover =>
WindowsDesktopUtilities.WinAudibleImageServer.GetImage(book.PictureId, FileManager.PictureSize._80x80);
public string Title
{
get
{
displayValues[nameof(Title)] = book.Title;
return getSortName(book.Title);
}
}
public string Authors => book.AuthorNames;
public string Narrators => book.NarratorNames;
public int Length
{
get
{
displayValues[nameof(Length)]
= book.LengthInMinutes == 0
? ""
: $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min";
return book.LengthInMinutes;
}
}
public string Series
{
get
{
displayValues[nameof(Series)] = book.SeriesNames;
return getSortName(book.SeriesNames);
}
}
private static string[] sortPrefixIgnores { get; } = new[] { "the", "a", "an" };
private static string getSortName(string unformattedName)
{
var sortName = unformattedName
.Replace("|", "")
.Replace(":", "")
.ToLowerInvariant()
.Trim();
if (sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " ")))
sortName = sortName.Substring(sortName.IndexOf(" ") + 1).TrimStart();
return sortName;
}
private string descriptionCache = null;
public string Description
{
get
{
// HtmlAgilityPack is expensive. cache results
if (descriptionCache is null)
{
if (book.Description is null)
descriptionCache = "";
else
{
var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(book.Description);
var noHtml = doc.DocumentNode.InnerText;
descriptionCache
= noHtml.Length < 63
? noHtml
: noHtml.Substring(0, 60) + "...";
}
}
return descriptionCache;
}
}
public string Category => string.Join(" > ", book.CategoriesNames);
// star ratings retain numeric value but display star text. this is needed because just using star text doesn't sort correctly:
// - star
// - star star
// - star 1/2
public string Product_Rating
{
get
{
displayValues[nameof(Product_Rating)] = starString(book.Rating);
return firstScore(book.Rating);
}
}
public string Purchase_Date
{
get
{
displayValues[nameof(Purchase_Date)] = libraryBook.DateAdded.ToString("d");
return libraryBook.DateAdded.ToString("yyyy-MM-dd HH:mm:ss");
}
}
public string My_Rating
{
get
{
displayValues[nameof(My_Rating)] = starString(book.UserDefinedItem.Rating);
return firstScore(book.UserDefinedItem.Rating);
}
}
private string starString(Rating rating)
=> (rating?.FirstScore != null && rating?.FirstScore > 0f)
? rating?.ToStarString()
: "";
private string firstScore(Rating rating) => rating?.FirstScore.ToString("0.0");
// max 5 text rows
public string Misc
{
get
{
var details = new List<string>();
var locale
= string.IsNullOrWhiteSpace(book.Locale)
? "[unknown]"
: book.Locale;
var acct
= string.IsNullOrWhiteSpace(libraryBook.Account)
? "[unknown]"
: libraryBook.Account;
details.Add($"Account: {locale} - {acct}");
if (book.HasPdf)
details.Add("Has PDF");
if (book.IsAbridged)
details.Add("Abridged");
if (book.DatePublished.HasValue)
details.Add($"Date pub'd: {book.DatePublished.Value:MM/dd/yyyy}");
// this goes last since it's most likely to have a line-break
if (!string.IsNullOrWhiteSpace(book.Publisher))
details.Add($"Pub: {book.Publisher.Trim()}");
if (!details.Any())
return "[details not imported]";
return string.Join("\r\n", details);
}
}
}
}

View File

@@ -1,34 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>libation.ico</ApplicationIcon>
<PublishTrimmed>true</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
<ProjectReference Include="..\WindowsDesktopUtilities\WindowsDesktopUtilities.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -1,191 +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();
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
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();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
this.SuspendLayout();
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.GridEntry);
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
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.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageColumn1
//
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
this.dataGridViewImageColumn1.HeaderText = "Cover";
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
this.dataGridViewImageColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn1
//
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
this.dataGridViewTextBoxColumn1.ReadOnly = true;
//
// 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;
//
// 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;
//
// dataGridViewTextBoxColumn8
//
this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating";
this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating";
this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8";
this.dataGridViewTextBoxColumn8.ReadOnly = true;
//
// dataGridViewTextBoxColumn9
//
this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date";
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date";
this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9";
this.dataGridViewTextBoxColumn9.ReadOnly = true;
//
// dataGridViewTextBoxColumn10
//
this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating";
this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating";
this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10";
this.dataGridViewTextBoxColumn10.ReadOnly = true;
//
// dataGridViewTextBoxColumn11
//
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
this.dataGridViewTextBoxColumn11.ReadOnly = true;
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.gridEntryDataGridView);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(434, 329);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.BindingSource gridEntryBindingSource;
private System.Windows.Forms.DataGridView gridEntryDataGridView;
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;
}
}

View File

@@ -1,406 +0,0 @@
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.Collections.Generic;
using Dinah.Core.DataBinding;
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. drowdown: DataGridView
// - drag/drop ProductItem on design surface
public partial class ProductsGrid : UserControl
{
public event EventHandler<int> VisibleCountChanged;
public event EventHandler BackupCountsChanged;
private const string EDIT_TAGS = "Edit Tags";
private const string LIBERATE = "Liberate";
// alias
private DataGridView dataGridView => gridEntryDataGridView;
public ProductsGrid()
{
InitializeComponent();
formatDataGridView();
addLiberateButtons();
addEditTagsButtons();
formatColumns();
manageLiveImageUpdateSubscriptions();
}
private void formatDataGridView()
{
dataGridView.Dock = DockStyle.Fill;
dataGridView.AllowUserToAddRows = false;
dataGridView.AllowUserToDeleteRows = false;
dataGridView.AutoGenerateColumns = false;
dataGridView.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dataGridView.DefaultCellStyle.WrapMode = DataGridViewTriState.True;
dataGridView.ReadOnly = true;
dataGridView.RowHeadersVisible = false;
// adjust height for 80x80 pictures.
// this must be done before databinding. or can alter later by iterating through rows
dataGridView.RowTemplate.Height = 82;
dataGridView.CellFormatting += replaceFormatted;
dataGridView.CellFormatting += hiddenFormatting;
// sorting breaks filters. must reapply filters after sorting
dataGridView.Sorted += (_, __) => filter();
}
#region format text cells. ie: not buttons
private void replaceFormatted(object sender, DataGridViewCellFormattingEventArgs e)
{
var col = ((DataGridView)sender).Columns[e.ColumnIndex];
if (col is DataGridViewTextBoxColumn textCol && getGridEntry(e.RowIndex).TryDisplayValue(textCol.Name, out string value))
{
// DO NOT DO THIS: getCell(e).Value = value;
// it's the wrong way and will infinitely call CellFormatting on each assign
// this is the correct way. will actually set FormattedValue (and EditedFormattedValue) while leaving Value as-is for sorting
e.Value = value;
getCell(e).ToolTipText = value;
}
}
private void hiddenFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
var dgv = (DataGridView)sender;
// no action needed for buttons
if (e.RowIndex < 0 || dgv.Columns[e.ColumnIndex] is DataGridViewButtonColumn)
return;
var isHidden = getGridEntry(e.RowIndex).TagsEnumerated.Contains("hidden");
getCell(e).Style
= isHidden
? new DataGridViewCellStyle { ForeColor = Color.LightGray }
: dgv.DefaultCellStyle;
}
#endregion
#region liberation buttons
private void addLiberateButtons()
{
dataGridView.Columns.Insert(0, new DataGridViewButtonColumn { HeaderText = LIBERATE });
dataGridView.CellPainting += liberate_Paint;
dataGridView.CellContentClick += liberate_Click;
}
private void liberate_Paint(object sender, DataGridViewCellPaintingEventArgs e)
{
if (!isColumnValid(e, LIBERATE))
return;
var cell = getCell(e);
var gridEntry = getGridEntry(e.RowIndex);
var liberatedStatus = gridEntry.Liberated_Status;
var pdfStatus = gridEntry.Pdf_Status;
// mouseover text
{
var libState = liberatedStatus switch
{
GridEntry.LiberatedState.Liberated => "Liberated",
GridEntry.LiberatedState.PartialDownload => "File has been at least\r\npartially downloaded",
GridEntry.LiberatedState.NotDownloaded => "Book NOT downloaded",
_ => throw new Exception("Unexpected liberation state")
};
var pdfState = pdfStatus switch
{
GridEntry.PdfState.Downloaded => "\r\nPDF downloaded",
GridEntry.PdfState.NotDownloaded => "\r\nPDF NOT downloaded",
GridEntry.PdfState.NoPdf => "",
_ => throw new Exception("Unexpected PDF state")
};
var text = libState + pdfState;
if (liberatedStatus == GridEntry.LiberatedState.NotDownloaded ||
liberatedStatus == GridEntry.LiberatedState.PartialDownload ||
pdfStatus == GridEntry.PdfState.NotDownloaded)
text += "\r\nClick to complete";
//DEBUG//cell.Value = text;
cell.ToolTipText = text;
}
// draw img
{
var image_lib
= liberatedStatus == GridEntry.LiberatedState.NotDownloaded ? "red"
: liberatedStatus == GridEntry.LiberatedState.PartialDownload ? "yellow"
: liberatedStatus == GridEntry.LiberatedState.Liberated ? "green"
: throw new Exception("Unexpected liberation state");
var image_pdf
= pdfStatus == GridEntry.PdfState.NoPdf ? ""
: pdfStatus == GridEntry.PdfState.NotDownloaded ? "_pdf_no"
: pdfStatus == GridEntry.PdfState.Downloaded ? "_pdf_yes"
: throw new Exception("Unexpected PDF state");
var image = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
drawImage(e, image);
}
}
private async void liberate_Click(object sender, DataGridViewCellEventArgs e)
{
if (!isColumnValid(e, LIBERATE))
return;
var productId = getGridEntry(e.RowIndex).GetBook().AudibleProductId;
// liberated: open explorer to file
if (TransitionalFileLocator.Audio_Exists(productId))
{
var filePath = TransitionalFileLocator.Audio_GetPath(productId);
if (!Go.To.File(filePath))
MessageBox.Show($"File not found:\r\n{filePath}");
return;
}
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(productId, (_, __) => RefreshRow(productId));
}
#endregion
public void RefreshRow(string productId)
{
var rowId = getRowId((ge) => ge.GetBook().AudibleProductId == productId);
// update cells incl Liberate button text
dataGridView.InvalidateRow(rowId);
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
filter();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
#region tag buttons
private void addEditTagsButtons()
{
dataGridView.Columns.Add(new DataGridViewButtonColumn { HeaderText = EDIT_TAGS });
dataGridView.CellPainting += editTags_Paint;
dataGridView.CellContentClick += editTags_Click;
}
private void editTags_Paint(object sender, DataGridViewCellPaintingEventArgs e)
{
// DataGridView Image for Button Column: https://stackoverflow.com/a/36253883
if (!isColumnValid(e, EDIT_TAGS))
return;
var cell = getCell(e);
var gridEntry = getGridEntry(e.RowIndex);
var displayTags = gridEntry.TagsEnumerated.ToList();
if (displayTags.Any())
cell.Value = string.Join("\r\n", displayTags);
else
{
// if removing all tags: clear previous tag text
cell.Value = "";
drawImage(e, Properties.Resources.edit_tags_25x25);
}
}
private void editTags_Click(object sender, DataGridViewCellEventArgs e)
{
// handle grid button click: https://stackoverflow.com/a/13687844
var dgv = (DataGridView)sender;
if (!isColumnValid(e, EDIT_TAGS))
return;
var liveGridEntry = getGridEntry(e.RowIndex);
// EditTagsDialog should display better-formatted title
liveGridEntry.TryDisplayValue(nameof(liveGridEntry.Title), out string value);
var editTagsForm = new EditTagsDialog(value, liveGridEntry.Tags);
if (editTagsForm.ShowDialog() != DialogResult.OK)
return;
var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
if (qtyChanges == 0)
return;
// force a re-draw, and re-apply filters
// needed to update text colors
dgv.InvalidateRow(e.RowIndex);
filter();
}
#endregion
private static void drawImage(DataGridViewCellPaintingEventArgs e, Bitmap image)
{
e.Paint(e.CellBounds, DataGridViewPaintParts.All);
var w = image.Width;
var h = image.Height;
var x = e.CellBounds.Left + (e.CellBounds.Width - w) / 2;
var y = e.CellBounds.Top + (e.CellBounds.Height - h) / 2;
e.Graphics.DrawImage(image, new Rectangle(x, y, w, h));
e.Handled = true;
}
private bool isColumnValid(DataGridViewCellEventArgs e, string colName) => isColumnValid(e.RowIndex, e.ColumnIndex, colName);
private bool isColumnValid(DataGridViewCellPaintingEventArgs e, string colName) => isColumnValid(e.RowIndex, e.ColumnIndex, colName);
private bool isColumnValid(int rowIndex, int colIndex, string colName)
{
var col = dataGridView.Columns[colIndex];
return rowIndex >= 0 && col.Name == colName && col is DataGridViewButtonColumn;
}
private void formatColumns()
{
for (var i = dataGridView.ColumnCount - 1; i >= 0; i--)
{
var col = dataGridView.Columns[i];
// initial HeaderText is the lookup name from GridEntry class. any formatting below won't change this
col.Name = col.HeaderText;
if (!(col is DataGridViewImageColumn || col is DataGridViewButtonColumn))
col.SortMode = DataGridViewColumnSortMode.Automatic;
col.HeaderText = col.HeaderText.Replace("_", " ");
col.Width = col.Name switch
{
LIBERATE => 70,
nameof(GridEntry.Cover) => 80,
nameof(GridEntry.Title) => col.Width * 2,
nameof(GridEntry.Misc) => (int)(col.Width * 1.35),
var n when n.In(nameof(GridEntry.My_Rating), nameof(GridEntry.Product_Rating)) => col.Width + 8,
_ => col.Width
};
}
}
#region live update newly downloaded and cached images
private void manageLiveImageUpdateSubscriptions()
{
FileManager.PictureStorage.PictureCached += crossThreadImageUpdate;
Disposed += (_, __) => FileManager.PictureStorage.PictureCached -= crossThreadImageUpdate;
}
private void crossThreadImageUpdate(object _, string pictureId)
=> dataGridView.UIThread(() => updateRowImage(pictureId));
private void updateRowImage(string pictureId)
{
var rowId = getRowId((ge) => ge.GetBook().PictureId == pictureId);
if (rowId > -1)
dataGridView.InvalidateRow(rowId);
}
#endregion
private bool hasBeenDisplayed = false;
public void Display()
{
if (hasBeenDisplayed)
return;
hasBeenDisplayed = true;
//
// transform into sorted GridEntry.s BEFORE binding
//
using var context = DbContexts.GetContext();
var lib = context.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 => new GridEntry(lb)).ToList()
// default load order
.OrderByDescending(ge => ge.Purchase_Date)
//// 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 = orderedGridEntries.ToSortableBindingList();
//
// FILTER
//
filter();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
#region filter
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 currencyManager = (CurrencyManager)BindingContext[dataGridView.DataSource];
currencyManager.SuspendBinding();
{
for (var r = dataGridView.RowCount - 1; r >= 0; r--)
dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).GetBook().AudibleProductId);
}
currencyManager.ResumeBinding();
VisibleCountChanged?.Invoke(this, dataGridView.AsEnumerable().Count(r => r.Visible));
}
#endregion
private int getRowId(Func<GridEntry, bool> func) => dataGridView.GetRowIdOfBoundItem(func);
private GridEntry getGridEntry(int rowIndex) => dataGridView.GetBoundItem<GridEntry>(rowIndex);
private DataGridViewCell getCell(DataGridViewCellFormattingEventArgs e) => getCell(e.RowIndex, e.ColumnIndex);
private DataGridViewCell getCell(DataGridViewCellPaintingEventArgs e) => getCell(e.RowIndex, e.ColumnIndex);
private DataGridViewCell getCell(int rowIndex, int columnIndex) => dataGridView.Rows[rowIndex].Cells[columnIndex];
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

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