Compare commits

...

376 Commits

Author SHA1 Message Date
Robert McRackan
0a54f8d881 v5.2.1 2021-07-20 14:47:15 -04:00
Robert McRackan
d31121e307 Bug fix to address #59 2021-07-20 14:43:08 -04:00
Robert McRackan
b86bd76726 New button in settings to open file logs folder 2021-07-20 14:37:32 -04:00
Robert McRackan
c49edbc77b Better error logging. MessageBoxAlertAdmin to make it easier for users to report errors 2021-07-20 14:27:27 -04:00
Robert McRackan
1ba54a74af rename basic/advanced settings in menu
*  basic => Settings
* advanced => Move settings files
2021-07-19 23:00:25 -04:00
Robert McRackan
9de08a332f v5.2.0 2021-07-19 14:53:10 -04:00
Robert McRackan
c9b434daed Overhaul of installation workflow per issue #36 2021-07-19 14:52:34 -04:00
rmcrackan
730484c28c Merge pull request #56 from Mbucari/master
Added MP3 support to settings.
2021-07-18 21:03:56 -04:00
Michael Bucari-Tovo
1a48dbe560 Fixed log. 2021-07-18 17:28:37 -06:00
Michael Bucari-Tovo
7df8c7427c Added MP3 conversion option. 2021-07-18 17:18:35 -06:00
Robert McRackan
8997f52505 replace dir selection controls in both settings dialogs with new ctrl.s 2021-07-18 17:48:15 -04:00
Robert McRackan
e61418c677 upgrade download bugfix 2021-07-18 16:29:31 -04:00
Robert McRackan
491aa67a81 add thread safety 2021-07-18 10:44:54 -04:00
Robert McRackan
7b3c857042 unsafe migration helper: Get => TryGet 2021-07-17 08:13:23 -04:00
Robert McRackan
71617b9620 Added UNSAFE_MigrationHelper to help with upgrades 2021-07-16 23:06:59 -04:00
Robert McRackan
f94f66da94 Revamped advanced settings dialog to use new control 2021-07-16 17:02:21 -04:00
Robert McRackan
2243e2a124 Get rid of meta directories. Centralize directory logic. New UI component to revamp settings and installation 2021-07-16 16:21:02 -04:00
Robert McRackan
5d6e3ea3f3 cancel buttons should set dialog result to Cancel 2021-07-15 17:14:45 -04:00
Robert McRackan
3c1c718bc7 don't attempt to overwrite advanced settings file if contents are unchanged 2021-07-15 17:14:20 -04:00
Robert McRackan
20a9e4b651 Simple winforms work in modern .net core designer. Only keep edge cases which work best in .net framework designer 2021-07-15 14:43:10 -04:00
Robert McRackan
f0daa12bb7 Remove unused interfaces and param 2021-07-15 11:48:52 -04:00
rmcrackan
c6e1278e42 Merge pull request #54 from Mbucari/master
Added Mp3 support to AAXClean.
2021-07-15 08:15:43 -04:00
Michael Bucari-Tovo
f5e8e4cd7f Removed indeterminate progress bar because decryption stattup time is now insignificant. 2021-07-15 00:17:59 -06:00
Michael Bucari-Tovo
f986462809 Changed default output type to Mp4a. 2021-07-14 23:58:52 -06:00
Michael Bucari-Tovo
49f2112c42 Updated for new AAXClean pattern. 2021-07-14 23:52:46 -06:00
Robert McRackan
0ce4faaf29 - Improved debugging for login
- Add warning when in Verbose logging mode
- Settings: don't try to re-save settings which haven't changed
- Remove unused logging config
2021-07-14 15:51:29 -04:00
Robert McRackan
bfd494cf93 remove unused setting 2021-07-13 14:59:55 -04:00
Robert McRackan
dc7ec3b328 auto-gened files 2021-07-13 14:59:41 -04:00
Robert McRackan
8f2827108b update gitignore 2021-07-13 07:14:32 -04:00
rmcrackan
fdcaf5e534 Merge pull request #53 from Mbucari/master
Removed Ffmpeg and taglib. Updated AAXClean.
2021-07-13 06:56:29 -04:00
Michael Bucari-Tovo
732695c019 Handle possible corrupt partial download. 2021-07-12 23:27:58 -06:00
Michael Bucari-Tovo
2a2faf6f7b Use new AAXClean.AaxFile type and remove unused dependences. 2021-07-12 23:13:18 -06:00
rmcrackan
c653e17c3d Merge pull request #52 from Mbucari/master
Minor fix.
2021-07-11 22:47:01 -04:00
Michael Bucari-Tovo
833bc3a8f2 Merge branch 'master' of https://github.com/Mbucari/Libation 2021-07-11 15:18:53 -06:00
Michael Bucari-Tovo
11e63ae5a2 Minor fix. 2021-07-11 15:18:07 -06:00
Robert McRackan
827eaefd29 Decryption moved to external project: AAXClean 2021-07-11 12:26:04 -04:00
rmcrackan
8240a97f6d Merge pull request #51 from Mbucari/master
Replaced FFMpeg decryptor and taglib with AAXClean
2021-07-11 12:14:15 -04:00
Mbucari
b766e43656 Merge branch 'master' into master 2021-07-11 09:17:07 -06:00
Robert McRackan
7d805728cb Login cvf bug fix 2021-07-11 09:51:39 -04:00
Michael Bucari-Tovo
c3c8a6fa6b Replaced FFMpeg decryptor and taglib with AAXClean 2021-07-10 20:21:28 -06:00
Robert McRackan
f40df002a2 Begin session logging with form feed 2021-07-09 11:34:20 -04:00
Robert McRackan
3180ea993c Startup logging: which logging levels are enabled 2021-07-09 11:06:57 -04:00
Robert McRackan
9f2fd54018 It helps if you wire-up the event. D'oh! 2021-07-09 10:20:46 -04:00
Robert McRackan
07532f7e65 "check for update" should not include pre-releases 2021-07-08 21:00:36 -04:00
Robert McRackan
4bae07d36c debugging added 2021-07-08 20:43:14 -04:00
Robert McRackan
bf23503d67 Added MFA-choice log in. Seems to come up more with German users 2021-07-08 17:04:11 -04:00
Robert McRackan
aeeba0d567 update ver. api bug fix 2021-07-08 10:54:38 -04:00
Robert McRackan
e2f919d625 "approval needed" ui improved wording 2021-07-07 21:34:05 -04:00
Robert McRackan
e821eea333 Bug: fix "approval" login step 2021-07-07 15:53:47 -04:00
Robert McRackan
8f487894f5 Fixed logging bug in single-book liberation 2021-07-04 16:10:11 -04:00
Robert McRackan
cd3e0dba68 Remove validation against 0-length chapters. It is evidently allowed 2021-07-04 16:08:30 -04:00
rmcrackan
6f31d97763 Merge pull request #47 from Mbucari/master
Addressed two issues and some minor fixed.
2021-07-04 09:27:58 -04:00
Mbucari
fa5637a340 Merge branch 'rmcrackan:master' into master 2021-07-03 22:07:34 -06:00
Michael Bucari-Tovo
7ab209171b Merge branch 'master' of https://github.com/Mbucari/Libation 2021-07-03 22:07:08 -06:00
Michael Bucari-Tovo
6d856f73e7 Reused yellow stoplight to indicate and interrupted and resumable download. 2021-07-03 22:06:56 -06:00
Michael Bucari-Tovo
05426eb618 Added uri refresh to step 2. 2021-07-03 21:54:28 -06:00
Michael Bucari-Tovo
d73701c939 Stop automatic processing if form is closed instead of crashing. 2021-07-03 20:34:50 -06:00
Michael Bucari-Tovo
f284f53edd Clicking on red stoplight now only decrypts that book with no conformation. 2021-07-03 20:21:11 -06:00
rmcrackan
17f3187748 Merge pull request #46 from Mbucari/master
Added resumable download support to FFMpegAaxcProcessor.
2021-07-03 21:06:25 -04:00
Mbucari
f55a41ac0a Merge branch 'rmcrackan:master' into master 2021-07-03 19:00:56 -06:00
Michael Bucari-Tovo
0be2a17537 Made FFMpegAaxcProcesser use NetworkFileStream. 2021-07-03 18:59:18 -06:00
rmcrackan
b417c5695e Merge pull request #45 from Mbucari/master
Fixed critical bug with Read stream not blocking.
2021-07-03 20:55:33 -04:00
Michael Bucari-Tovo
6efe064ca7 Added support for changing Uri to the same file in case iold one expires. 2021-07-03 17:15:35 -06:00
Michael Bucari-Tovo
da7af895fb Fixed possible hang issue. 2021-07-03 14:37:24 -06:00
Mbucari
1b39f30fd0 Merge branch 'rmcrackan:master' into master 2021-07-03 14:31:23 -06:00
Michael Bucari-Tovo
9cde6bddbd Fixed Read not blocking 2021-07-03 14:31:02 -06:00
rmcrackan
b21f257baa Merge pull request #43 from Mbucari/master
Modified NetworkFileStream to make it resumable.
2021-07-03 11:14:07 -04:00
Michael Bucari-Tovo
da68ddc9b8 Renamed property. 2021-07-03 06:13:38 -06:00
Michael Bucari-Tovo
9e15fde2e3 Modified NetworkFileStream to make is resumable. 2021-07-03 06:10:51 -06:00
rmcrackan
ef5b14a929 Merge pull request #40 from Mbucari/master
Addressed Issue #37 and minor corrections
2021-07-02 23:48:44 -04:00
Michael Bucari-Tovo
5df7d80aac Revert earlier. 2021-07-02 21:21:17 -06:00
Michael Bucari-Tovo
4b2c8ee513 Add any subtitle to the title. 2021-07-02 17:00:04 -06:00
Michael Bucari-Tovo
097bda2d25 Added null check. 2021-07-02 15:58:37 -06:00
Michael Bucari-Tovo
81195e382e Revert "Remove items from library."
This reverts commit 00f7e4b779.
2021-07-02 15:24:05 -06:00
Michael Bucari-Tovo
35fc3581b3 Revert "Added count of items removed from library."
This reverts commit 771d992da7.
2021-07-02 15:23:26 -06:00
Michael Bucari-Tovo
771d992da7 Added count of items removed from library. 2021-07-02 15:01:55 -06:00
Michael Bucari-Tovo
00f7e4b779 Remove items from library. 2021-07-02 14:07:42 -06:00
Michael Bucari-Tovo
5d4bcb2db0 Removed unnecessary conversion to List. 2021-07-02 13:51:32 -06:00
Mbucari
fbf92bf151 Merge branch 'rmcrackan:master' into master 2021-07-02 08:42:14 -06:00
Robert McRackan
2cb3e34d98 update 5.0.0.x ver # 2021-07-02 10:02:37 -04:00
rmcrackan
80589e3854 Update README.md
Add "Allow Libation to fix up audiobook metadata" to readme
2021-07-02 09:52:49 -04:00
Michael Bucari-Tovo
b9770220db Fixed stupid mistake. I need to go to ber. 2021-07-01 21:56:38 -06:00
rmcrackan
11128ffb1a Merge pull request #34 from Mbucari/master
Minor code fixes and simplification
2021-07-01 23:53:38 -04:00
Michael Bucari-Tovo
1d557d05c5 Merge branch 'master' of https://github.com/Mbucari/Libation 2021-07-01 21:44:12 -06:00
Michael Bucari-Tovo
d41fe0d3e6 Added log 2021-07-01 21:42:44 -06:00
Mbucari
17bd54a897 Merge branch 'rmcrackan:master' into master 2021-07-01 18:01:37 -06:00
Michael Bucari-Tovo
0d89c34107 Added try blocks. TODO: Add error logging. 2021-07-01 18:01:03 -06:00
Michael Bucari-Tovo
66bd18fdc5 Fixed typo. 2021-07-01 17:58:02 -06:00
Robert McRackan
7143104b40 git ignore windows shortcuts: *.lnk files 2021-07-01 16:11:52 -04:00
Michael Bucari-Tovo
729212a5d5 Simplified ffmpeg chapter parser by using json insterad of xml. 2021-07-01 13:16:54 -06:00
Michael Bucari-Tovo
6dafa03554 Replaced property name string with nameof. 2021-07-01 13:14:54 -06:00
Robert McRackan
08644fb937 Move UnicodeToAscii to Dinah.Core, minor refactoring 2021-07-01 11:46:23 -04:00
Robert McRackan
7ff4953f7b RetainAaxFiles no longer used 2021-07-01 11:19:16 -04:00
rmcrackan
797112740e Merge pull request #33 from Mbucari/master
Work with updated Api Dtos and some minor readability edits.
2021-07-01 10:52:15 -04:00
Michael Bucari-Tovo
36ab494b31 Minor refactoring. 2021-07-01 08:22:04 -06:00
Michael Bucari-Tovo
8c6ada8d20 Removed unused namespaces. 2021-07-01 08:17:10 -06:00
Michael Bucari-Tovo
41b0ace238 Code readability edit. 2021-07-01 08:10:28 -06:00
Michael Bucari-Tovo
c84f144274 Merge branch 'master' of https://github.com/Mbucari/Libation 2021-07-01 07:56:50 -06:00
Michael Bucari-Tovo
00f8a63781 Use new AudibleApi Dtos. 2021-07-01 07:52:28 -06:00
Michael Bucari-Tovo
25d89207bb Better ecapsulate requestCoverArt event handler 2021-07-01 07:48:20 -06:00
rmcrackan
2146ebff29 Merge pull request #32 from Mbucari/master
Changes we discussed, plus a few more.
2021-07-01 08:45:37 -04:00
Michael Bucari-Tovo
3aed3a5def Added "indeterminate" progress bar mode to beginning of Step3. 2021-06-30 21:55:22 -06:00
Michael Bucari-Tovo
1ee6f3b9f2 Improved cancellation routine. 2021-06-30 18:33:32 -06:00
Michael Bucari-Tovo
0c26c34bdd Fixed improper condition check. 2021-06-30 18:11:56 -06:00
Michael Bucari-Tovo
6696317ae6 Changed DownloadChapters setting to AllowLibationFixup. 2021-06-30 17:38:24 -06:00
Michael Bucari-Tovo
3af84af2e2 Added events to retrieve cover art upon request using PictureStorage. 2021-06-30 17:32:34 -06:00
Michael Bucari-Tovo
2955e8b464 Added method to set cover art in tags for writing. 2021-06-30 17:31:30 -06:00
Michael Bucari-Tovo
8d6b304a8b Moved Taglib loading into the step sequence and added event and method to request cover art. 2021-06-30 17:31:01 -06:00
Michael Bucari-Tovo
aa3c648c4c Refactored remaining time estimate. 2021-06-30 13:21:39 -06:00
Michael Bucari-Tovo
0da054ccea Taglib fixes. 2021-06-30 12:20:21 -06:00
Michael Bucari-Tovo
45080d1661 Refactored tags. 2021-06-30 12:05:29 -06:00
Michael Bucari-Tovo
d6b62c0521 Removed obsolete assembly reference and minor refactoring. 2021-06-30 01:12:34 -06:00
Michael Bucari-Tovo
bc3aa29175 Minor tag reading improvements. 2021-06-30 01:06:22 -06:00
Michael Bucari-Tovo
e958944466 Added TagLib through package manager and improved tag retreival. 2021-06-30 00:54:52 -06:00
Michael Bucari-Tovo
78f278121b Add book info to DecryptForm from LibraryBook on initialization. 2021-06-29 22:04:54 -06:00
Michael Bucari-Tovo
027cce2d99 Remove DownloadBook from BackupBook. 2021-06-29 22:03:36 -06:00
Michael Bucari-Tovo
9332a6f350 Remove downlaod book events. 2021-06-29 22:02:47 -06:00
Michael Bucari-Tovo
ac6a73d898 Removed DownloadBookDummy and related process steps. 2021-06-29 22:00:05 -06:00
Michael Bucari-Tovo
74f94fe17f Add book tag null checks. 2021-06-29 21:56:08 -06:00
Robert McRackan
120fb58da7 Add null check when displaying image in form 2021-06-29 13:57:18 -04:00
Robert McRackan
ef2adfd474 If a failure occurs before aaxcProcesser is init'd, this will give a null ref exception which can obscure the actual exception that we want to be visible 2021-06-29 13:08:21 -04:00
Robert McRackan
f3a746a852 Make path safe. Colon was breaking file path 2021-06-29 12:02:04 -04:00
Robert McRackan
dc8cea5355 fix ETA label anchor 2021-06-29 12:01:29 -04:00
Robert McRackan
83cb580db7 remove fragile broken tests 2021-06-29 10:29:31 -04:00
rmcrackan
491a5eba3a Merge pull request #30 from Mbucari/master
Complete overhaul of download and decrypt.
2021-06-29 09:35:19 -04:00
Michael Bucari-Tovo
15150a3633 Added UpdateRemainingTime event unsubscribe. 2021-06-28 15:46:24 -06:00
Michael Bucari-Tovo
82e3854c84 Fixed typo and minor formatting. 2021-06-28 15:35:43 -06:00
Michael Bucari-Tovo
f0eb57a40b Added Cancel method to stop download/decrypt and added estimated time remaining event. 2021-06-28 15:24:33 -06:00
Michael Bucari-Tovo
b65f9567e0 Added user option to download chapter titles from Audible. 2021-06-28 12:55:56 -06:00
Michael Bucari-Tovo
b5389c67ea Get chapter infor from file is no downloaded chapters were supplied. 2021-06-28 12:51:28 -06:00
Michael Bucari-Tovo
d564876eaa Removed unused option to retain aax files. 2021-06-28 11:24:33 -06:00
Michael Bucari-Tovo
258887152d Removed taglib assembly reference. 2021-06-28 11:21:54 -06:00
Michael Bucari-Tovo
87c3cac013 Fixed improper async usage. 2021-06-28 11:20:38 -06:00
Michael Bucari-Tovo
f148650e57 Fixed ffmpeg cover art not showing on android as discussed. 2021-06-28 11:19:03 -06:00
Michael Bucari-Tovo
b53aabe0e3 Modified ffmpeg arguments and added argument checks. 2021-06-27 18:24:44 -06:00
Michael Bucari-Tovo
e32a39085f Fixed narrator fields. 2021-06-26 02:09:03 -06:00
Michael Bucari-Tovo
4d743df643 Fixed ProcessBook overload to preserve chapters. 2021-06-26 01:58:23 -06:00
Michael Bucari-Tovo
6bd809c7c6 Minor edits and reorganizations. 2021-06-26 01:52:18 -06:00
Michael Bucari-Tovo
9930daa914 Made changes discussed in pull request. 2021-06-26 01:39:18 -06:00
Michael Bucari-Tovo
0475bd48b1 Typos and minor corrections. 2021-06-24 21:49:52 -06:00
Michael Bucari-Tovo
1b80f2ed28 Improve progress update. 2021-06-24 18:18:52 -06:00
Michael Bucari-Tovo
37ca9abd9d Merge branch 'master' of https://github.com/Mbucari/Libation 2021-06-24 18:13:11 -06:00
Michael Bucari-Tovo
0c159df6ca Simplified download decrypt progress update.
Removed unused fields and properties.
2021-06-24 18:13:00 -06:00
Michael Bucari-Tovo
31e24ad36c Removed unused fields and properties.
Removed unused fields.
2021-06-24 18:11:44 -06:00
Michael Bucari-Tovo
be41dca9e0 Sumplified download decrypt progress update. 2021-06-24 18:09:43 -06:00
Michael Bucari-Tovo
792207caee Fixed type. 2021-06-24 17:56:08 -06:00
Michael Bucari-Tovo
06549e5b4e Added AtomicParsley back in because ffmpeg cover art wasn't working. 2021-06-24 17:53:09 -06:00
Michael Bucari-Tovo
81d0f87b8a Removed unused classes. 2021-06-24 17:05:21 -06:00
Michael Bucari-Tovo
9550aac788 Merge branch 'master' of https://github.com/Mbucari/Libation 2021-06-24 16:57:51 -06:00
Michael Bucari-Tovo
54d650ea48 Decrypt libs and taglib-sharp reference 2021-06-24 16:57:12 -06:00
Michael Bucari-Tovo
1e88070f3a Completely redesigned donload and decrypt. 2021-06-24 16:48:29 -06:00
Robert McRackan
703e71ad74 Clean up minutia 2021-06-22 22:36:16 -04:00
Robert McRackan
ae6384486c All downloads are now aaxc, not aax 2021-06-22 22:26:39 -04:00
rmcrackan
8f8e0645a4 Merge pull request #29 from Mbucari/master
Add major version migration and hacked avformat-58
2021-06-22 21:37:07 -04:00
Michael Bucari-Tovo
d619c82fd8 Added progress bar to Chapterize step. 2021-06-22 17:30:48 -06:00
Michael Bucari-Tovo
919175cc10 Fixed migration logic. 2021-06-22 16:55:56 -06:00
Michael Bucari-Tovo
8d70d2a95f Fixed migration logic. 2021-06-22 16:53:02 -06:00
Michael Bucari-Tovo
e13dc2a48a Fixed migration logic. 2021-06-22 16:48:43 -06:00
Michael Bucari-Tovo
bb3baa6ce0 Added explanation of avformat-58 hack. 2021-06-22 16:36:23 -06:00
Mbucari
28731e51f5 Merge pull request #1 from rmcrackan/master
Pull in Robert's changes.
2021-06-22 15:15:53 -06:00
Michael Bucari-Tovo
dbcd124c1d Match version 2021-06-22 15:15:08 -06:00
Robert McRackan
305de34a76 db migration to remove license keys from Books table. They are not really data related to a book. Also, it was causing problems on update due to other persistence choices. For now, store decrypt keys along side of encrypted file instead. 2021-06-22 17:02:00 -04:00
Michael Bucari-Tovo
0034d51921 Add migration to re-register device fore new Identity properties. 2021-06-22 14:59:37 -06:00
Robert McRackan
b1a033e162 Keep download license details with aax file, not in db 2021-06-22 16:49:19 -04:00
Robert McRackan
9416f4e040 Persist keys to db. Necessary in the event that download succeeds then decrypt fails. 2021-06-22 11:25:18 -04:00
Michael Bucari-Tovo
344e675634 Hacked avformat-58.dll to ignore aac Scalable configurations flag. 2021-06-21 16:38:02 -06:00
Michael Bucari-Tovo
372e85d9af Merge branch 'master' of https://github.com/Mbucari/Libation 2021-06-21 16:29:51 -06:00
Michael Bucari-Tovo
c81788429b Revert "Hacked avformat-58 to ignore aac Scalable configurations error."
This reverts commit 26da307743.
2021-06-21 16:29:28 -06:00
Michael Bucari-Tovo
26da307743 Hacked avformat-58 to ignore aac Scalable configurations error. 2021-06-21 16:23:44 -06:00
Robert McRackan
0306c958d1 Remove filtering-out audible plus titles during library import. ie: they are now included 2021-06-21 15:10:40 -04:00
Robert McRackan
5ec6994da7 Remove outdated proof of concept demo programs 2021-06-21 14:24:54 -04:00
rmcrackan
c1f50a184a Merge pull request #27 from Mbucari/master
Add support for AXXC
2021-06-21 14:01:02 -04:00
Michael Bucari-Tovo
8231766d2c Fixed typo and removed unused method. 2021-06-21 10:07:30 -06:00
Michael Bucari-Tovo
eedc9bb34d Removed unused libraries and their references. 2021-06-21 10:04:48 -06:00
Michael Bucari-Tovo
310b90962c Download and decrypt AAXC files. Upgraded ffmpeg to 4.4-19. 2021-06-19 00:59:39 -06:00
Michael Bucari-Tovo
54c21e969e Added AAXC decryption keys to Book and created Migration. 2021-06-18 21:03:05 -06:00
Robert McRackan
ff20d777a6 Remove local retrieval of activation bytes. Use audible api. Dramatically reduces program's size 2021-06-17 17:02:37 -04:00
Robert McRackan
270e2531e2 update version 2021-06-17 14:49:05 -04:00
Robert McRackan
959a1aebe9 Enough already. I'm obviously not incentivized/shamed into writing unit tests for things in UNTESTED dir.s. It's just making a mess of the file tree 2021-06-17 14:21:15 -04:00
rmcrackan
2217fe6948 Merge pull request #26 from Mbucari/master
Added support for downloaded chapter titles.
2021-06-17 13:37:15 -04:00
Robert McRackan
96abf56a87 remove unused PublishSingleFile directive 2021-06-17 11:45:44 -04:00
Michael Bucari-Tovo
5731a8f693 Added support for downloaded chapters. 2021-06-16 17:04:42 -06:00
Michael Bucari-Tovo
ff722b6a52 Added support for chapter titles and refactored. 2021-06-16 16:58:01 -06:00
Michael Bucari-Tovo
9271114408 Allow caller to specify alternate chapters source. 2021-06-16 16:27:23 -06:00
Michael Bucari-Tovo
ebfdd44142 Abstracted Chapters class, adding chapter titles and end times. Updated references. 2021-06-16 16:05:06 -06:00
Robert McRackan
6ed4eb34bd update references. move db scratch pad into test 2021-05-06 11:53:40 -04:00
Robert McRackan
9372571370 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-04-12 14:52:43 -04:00
Robert McRackan
215c539920 Bug fix: first line in cue file was incorrectly formatted 2021-04-12 14:52:19 -04:00
rmcrackan
7c7da2024e Update README.md
Add paypal link
2021-04-08 13:56:39 -04:00
Robert McRackan
f55a3ca008 Search engine bug fix and unit tests 2021-04-02 11:27:16 -04:00
Robert McRackan
726b36de4d * bug fix: when user creates a tag which is also a reserved bool word (eg: israted), searching for this tag breaks the search
* add unit tests for search engine
2021-04-01 15:45:19 -04:00
Robert McRackan
abd00ff1df * search engine: refactoring and improved logging
* bug fix: after book is liberated, filter should immediately honor new "is liberated" status
2021-04-01 12:44:16 -04:00
Robert McRackan
7b966f6962 Add IsLiberated option to search engine. Reindex search after download and decrypt 2021-03-31 21:50:32 -04:00
Robert McRackan
c0e955d5ef Better logging and error handling during login 2021-03-15 13:30:33 -04:00
Robert McRackan
bc6f53c8ea bug fix around latest skip-bad-book feature 2020-12-23 16:07:47 -05:00
Robert McRackan
cefab86ce1 bug fixes around new skip-bad-book feature 2020-12-23 14:05:18 -05:00
Robert McRackan
249a2f3b59 bug fix: libhack files: directory not found 2020-12-22 15:54:46 -05:00
Robert McRackan
0e9f2c7681 Truncate too-long error message 2020-12-22 11:38:03 -05:00
Robert McRackan
d25c32ff45 When there's a problem downloading a book, you get the option to skip the file temporarily or permanently. This can be useful with extremely old audible titles where the modern download may no longer be supported 2020-12-21 16:25:42 -05:00
Robert McRackan
642a500f87 "Locale" typo. Make user msg more clear 2020-12-16 12:57:26 -05:00
Robert McRackan
0e2469db64 If no codec during download, retry with all library flags enabled 2020-12-16 11:19:12 -05:00
Robert McRackan
9aa4ef70af increase version 2020-12-14 15:56:30 -05:00
Robert McRackan
1812fc2c7c - Increase account privacy in logs
- Improve book download retry
2020-12-14 15:42:27 -05:00
Robert McRackan
f9849abb7b Better error logging when codecs are not known 2020-12-08 16:16:16 -05:00
Robert McRackan
9cfe8ee6ca bug fix: null ref exception 2020-12-03 10:58:45 -05:00
Robert McRackan
44e2cef18c New in v4.1.0
- upgrade all to .NET5
- bug fix: when codec doesn't appear in prioritized list, just get the 1st available
- add more account privacy in logs
2020-12-02 15:23:08 -05:00
Robert McRackan
4dc29affc3 Incl. audible api bug fix. Also add more account privacy in logs 2020-12-02 15:16:48 -05:00
Robert McRackan
2df38706f7 upgrade to .NET5 2020-11-18 09:32:15 -05:00
Robert McRackan
f30e9dae6f bug fix: include new ApprovalNeeded in enumeration singletons 2020-10-08 22:20:26 -04:00
Robert McRackan
50843e5102 add ApprovalNeeded page in login 2020-10-08 16:56:14 -04:00
Robert McRackan
a13b00d520 - better logging for LoginFailedException
- upgrade nuget pkg.s
2020-10-08 11:48:48 -04:00
Robert McRackan
b5ebe3db23 increm version 2020-10-07 17:48:30 -04:00
Robert McRackan
40f3e4503b Version # 2020-10-02 16:22:42 -04:00
Robert McRackan
0d93243b66 Obscure account names in logs 2020-10-02 16:10:35 -04:00
Robert McRackan
59c3845d21 Standardize logging 2020-10-02 09:35:58 -04:00
Robert McRackan
a3ee3c2881 v4.0.9 2020-10-01 12:35:16 -04:00
Robert McRackan
e971d34948 Bug fix: downloading PDFs without also Liberating books -- post-download verification step was failing 2020-09-22 15:43:13 -04:00
Robert McRackan
2b3f67fb99 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-09-21 13:10:45 -04:00
Robert McRackan
4509b8c8eb Audible whack-a-mole: they changed how to download pdfs 2020-09-21 13:10:36 -04:00
rmcrackan
2e40bebd7d Update README.md 2020-09-11 22:12:25 -04:00
Robert McRackan
dfc4121ab0 add pics for readme 2020-09-11 22:09:44 -04:00
Robert McRackan
3648607d4d export to xlsx 2020-09-11 21:56:56 -04:00
Robert McRackan
b22c35f841 new feature: json export 2020-09-11 21:07:20 -04:00
Robert McRackan
2795690199 New feature: csv export 2020-09-11 17:04:36 -04:00
Robert McRackan
b1f92343cf increm version 2020-09-10 09:07:04 -04:00
Robert McRackan
9e1d657f60 Config setting to retain aax file after decrypt 2020-09-10 09:06:34 -04:00
Robert McRackan
389761355d New version. Failed attempt to fix publish error 2020-09-07 15:45:08 -04:00
Robert McRackan
69054afaa0 update version # 2020-09-01 09:54:55 -04:00
Robert McRackan
aacdcea1e1 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-09-01 09:35:38 -04:00
Robert McRackan
0beb3bf437 Add logging 2020-09-01 09:35:13 -04:00
rmcrackan
e925b57f7f Update README.md 2020-08-31 23:03:28 -04:00
rmcrackan
5deaa06d78 Update README.md 2020-08-31 22:57:47 -04:00
Robert McRackan
eda62975ba Screenshots 2020-08-31 22:57:27 -04:00
Robert McRackan
d91e02db29 Increase version. 4.0! 2020-08-31 22:30:50 -04:00
Robert McRackan
cd604d03b1 Fix v3 => v4 migration bug. Improved error handing 2020-08-31 21:27:56 -04:00
Robert McRackan
d5cd569319 clean up comments 2020-08-30 13:01:39 -04:00
Robert McRackan
a58f51a8ce Libation 4.0 prep: do not allow user to change login id in the middle of logging in. If they do then jsonpath will fail 2020-08-28 15:03:55 -04:00
Robert McRackan
d24c10ddf5 Pass account info to login dialogs 2020-08-28 13:55:03 -04:00
Robert McRackan
a12391f0ab Serialize getting API instances so that logins don't conflict 2020-08-28 10:51:42 -04:00
Robert McRackan
60f1d8117d Remove reliance on persistent Account objects across boundaries. If you open an account persister, then dispose of it 2020-08-27 23:05:46 -04:00
Robert McRackan
20b6f28cb5 Add locale and account to search/filter options 2020-08-27 15:33:14 -04:00
Robert McRackan
9a1fa89f6f Grid, misc column: incl locale and acct 2020-08-27 15:08:36 -04:00
Robert McRackan
2a294f4f85 add locale and account to logging 2020-08-27 14:54:11 -04:00
Robert McRackan
0938c84929 Do not import non-library 'audible plus' titles 2020-08-27 13:57:53 -04:00
Robert McRackan
99cc6a6425 Accounts are editable from ScanAccountsDialog via new edit button 2020-08-27 11:36:38 -04:00
Robert McRackan
0025825d5c If no accounts, Import>Scan Library to prompt user to create account 2020-08-27 10:46:47 -04:00
Robert McRackan
81b6833118 Incl note that defaults can be changed in account settings 2020-08-26 12:59:32 -04:00
Robert McRackan
a51e76d44d Libation 4.0 prep: full multiple account support 2020-08-26 12:50:12 -04:00
Robert McRackan
755a7338e9 Account to be included on each import item, not just on the aggr group 2020-08-26 10:25:24 -04:00
Robert McRackan
56732a5365 Add stub form for 'scan mult accounts' dialog 2020-08-25 16:34:06 -04:00
Robert McRackan
dd3b032b21 Fix account persistence edge case 2020-08-25 15:58:56 -04:00
Robert McRackan
dd25792864 Multi-account scan library UI stubs 2020-08-25 15:25:28 -04:00
Robert McRackan
6979ab4450 Libation 4.0 prep: account management complete 2020-08-25 14:21:14 -04:00
Robert McRackan
4b31207f91 Make AccountsSettings and Persister more clear 2020-08-25 10:34:55 -04:00
Robert McRackan
84a847a838 Fix unit test 2020-08-24 23:15:58 -04:00
Robert McRackan
6900a68b9d Rename for clarity: AccountsSettings <=> Accounts 2020-08-24 22:57:08 -04:00
Robert McRackan
743644c4e9 account management UI 2020-08-24 16:22:55 -04:00
Robert McRackan
e9e380dbe6 Account management UI, add 'original' column 2020-08-24 14:16:20 -04:00
Robert McRackan
515dfceb73 begin account management UI 2020-08-24 14:08:07 -04:00
Robert McRackan
3941906d72 Book download validation: librarybook.accountname and/or book.locale is null/blank 2020-08-22 07:32:39 -04:00
Robert McRackan
6407d15fe0 Libation 4.0 prep: use new Accounts decrypt key 2020-08-21 22:15:25 -04:00
Robert McRackan
be84fb317e DecryptKey: static => account instance 2020-08-21 22:01:23 -04:00
Robert McRackan
3af010c1f5 Update ProcessRunner 2020-08-21 17:03:39 -04:00
Robert McRackan
714bb2ba50 Downloading to use new instance locale and account 2020-08-21 13:36:01 -04:00
Robert McRackan
2e5360f0ba Libation 4.0 prep: purge global static config Configuration.LocaleCountryCode 2020-08-21 11:50:11 -04:00
Robert McRackan
258775ff3f Purge static current locale from API: complete 2020-08-21 11:18:10 -04:00
Robert McRackan
82318ffab7 Progress toward purging static current locale 2020-08-21 09:29:33 -04:00
Robert McRackan
901572e7bb intermediate steps toward purging static current locale 2020-08-20 22:53:44 -04:00
Robert McRackan
cfa938360a Library to store book's account in db 2020-08-20 17:03:55 -04:00
Robert McRackan
80017ce9fd populate book locale on library update 2020-08-20 16:49:50 -04:00
Robert McRackan
c67972a327 Importers need access to Account 2020-08-20 16:09:07 -04:00
Robert McRackan
57ee150d3c api to use hardcoded jsonpath for now. Libation is fully functional again 2020-08-20 14:47:26 -04:00
Robert McRackan
57302e1b5c Create methods for test and demo use. Can also use temporarily in Libation until migration is complete 2020-08-19 15:42:54 -04:00
Robert McRackan
09dbc67914 refactor file migration 2020-08-18 07:14:58 -04:00
Robert McRackan
b768362eae migrate old files/settings 2020-08-18 06:51:12 -04:00
Robert McRackan
04a32533cb Libation 4.0 prep: migrate old settings => new files. remove old values. remove old file 2020-08-18 06:32:45 -04:00
Robert McRackan
1ad2135a3f Complete: Accounts, persistence, unit tests 2020-08-16 23:04:23 -04:00
Robert McRackan
643ae09b2b Libation 4.0 prep: in process of migrating to new settings files 2020-08-16 15:44:47 -04:00
Robert McRackan
8391e43b03 begin token file migration. INCOMPLETE 2020-08-13 17:01:06 -04:00
Robert McRackan
8a54eda4a0 Add temp jsonpath options during v3 => v4 migration prep 2020-08-13 15:06:22 -04:00
Robert McRackan
e0406378cb Libation 4.0 prep: Add legacy settings-file support. Move AudibleApiStorage 2020-08-13 14:37:16 -04:00
Robert McRackan
e1299331cc "ApiConnectionSettings" => "AccountsSettings" 2020-08-13 14:26:48 -04:00
Robert McRackan
248b336867 minor csproj changes 2020-08-13 14:21:59 -04:00
Robert McRackan
b7d96ae447 tiny refactor 2020-08-13 09:53:29 -04:00
Robert McRackan
8ab2af1c5d Rename file path var for clarity 2020-08-13 09:14:16 -04:00
Robert McRackan
2d459bb2cf Libation 4.0 prep: add new fields Book.Locale, LibraryBook.Account. Migrate db 2020-08-12 11:31:48 -04:00
Robert McRackan
aeb0d2a82b update nuget pkg.s 2020-08-12 10:15:04 -04:00
Robert McRackan
f50dab94a4 Libation 4.0 prep: incrementally incorporate jsonpath (1) all AccountsSettings.json access must use centralized jsonpath. For now == null 2020-08-11 14:19:35 -04:00
Robert McRackan
efa5cefa23 Libation 4.0 prep. EzApiCreator: remove locale option, add JsonPath support, libation wrapper to pass in jsonpath 2020-08-10 10:31:41 -04:00
Robert McRackan
2e4a97fde7 4.0 prep:
move IdentityTokens data to new AccountsSettings.json file
2020-08-06 14:01:12 -04:00
Robert McRackan
2f241806fa version update 2020-07-31 14:32:51 -04:00
Robert McRackan
e417f60a36 When changing locale, clear previous locale specific settings 2020-07-31 14:21:59 -04:00
Robert McRackan
b00f2bd908 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-07-31 14:05:44 -04:00
Robert McRackan
220cda42e7 Downloader internationalization bug 2020-07-31 14:03:49 -04:00
rmcrackan
f992a7ec64 Update README.md
Add download link to 'getting started'
2020-07-03 18:15:03 -04:00
Robert McRackan
c54c45df33 Bugfix. Audible changed how they handle categories, causing a new bug. Temp fix to get everything working again -- part 2 2020-06-01 21:51:47 -04:00
Robert McRackan
a8b9e187e6 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-05-26 14:38:34 -04:00
Robert McRackan
53f252e56f Bugfix. Audible changed how they handle categories, causing a new bug. Temp fix to get everything working again 2020-05-26 14:38:29 -04:00
rmcrackan
2827bc8904 Update README.md 2020-05-25 07:22:06 -04:00
rmcrackan
98a775fc5a Update README.md 2020-05-25 07:21:35 -04:00
Robert McRackan
f28a729d36 forgot to increment build number 2020-04-18 22:27:59 -04:00
Robert McRackan
00a6a4bf50 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-04-18 22:23:04 -04:00
Robert McRackan
fdefa7c3bf Dependency updated. Increase version 2020-04-18 22:22:59 -04:00
rmcrackan
244862299f Update README.md
Add Australia
2020-03-04 09:47:20 -05:00
Robert McRackan
4decf9d3b7 Experimental: add Australia to locale options 2020-03-03 15:31:41 -05:00
Robert McRackan
83f538d304 Improved logging 2020-02-18 12:20:13 -05:00
Robert McRackan
9e0e06e436 Bugfix: some series indexes/sequences formats cause library not to import 2020-02-17 16:41:12 -05:00
Robert McRackan
f27ac279b2 Bugfix: some series indexes/sequences could cause library not to import 2020-02-17 15:17:59 -05:00
Robert McRackan
ed03fd2451 increment csproj version 2020-02-14 14:08:51 -05:00
Robert McRackan
ccb60ae367 Bugfix: IsAuthorNarrated was returning no books 2020-02-14 14:01:36 -05:00
Robert McRackan
6ad541c199 fix weirdness with build number 2020-01-02 09:01:04 -05:00
Robert McRackan
9606acda26 update release notes 2019-12-31 13:00:36 -05:00
Robert McRackan
9abb9e376d null checks 2019-12-31 10:53:15 -05:00
Robert McRackan
f93498bfe3 v3.1.1 release 2019-12-31 10:06:57 -05:00
Robert McRackan
a13e1f27bb clicking on stoplight prompts with option to liberate full library 2019-12-31 10:04:55 -05:00
Robert McRackan
c7c1b4505b improved logging: at startup, config changes, library counts
better starting instructions to liberate library
2019-12-31 09:49:32 -05:00
Robert McRackan
d9e0f1aedf New feature: check if upgrade available on github 2019-12-27 22:08:43 -05:00
Robert McRackan
d8a0124b68 Better error when download service is unavailable 2019-12-26 13:04:56 -05:00
Robert McRackan
79e0a8fba7 Merge branch 'master' of https://github.com/rmcrackan/Libation 2019-12-23 11:24:54 -05:00
Robert McRackan
8497987967 update release notes 2019-12-23 11:24:49 -05:00
rmcrackan
717fefd2c0 Update README.md 2019-12-23 11:17:00 -05:00
rmcrackan
066cae8e33 update readme 2019-12-23 11:15:32 -05:00
rmcrackan
9083574a77 Update README.md 2019-12-23 10:52:50 -05:00
rmcrackan
6b1ab9c777 Update README.md 2019-12-23 10:51:52 -05:00
rmcrackan
3be7c87c8e Update README.md 2019-12-23 10:46:52 -05:00
rmcrackan
8694d3206b Update README.md 2019-12-23 10:45:42 -05:00
Robert McRackan
f67f3805c6 add screenshot 2019-12-23 10:45:27 -05:00
Robert McRackan
612dd41b4b Merge branch 'master' of https://github.com/rmcrackan/Libation 2019-12-23 10:42:00 -05:00
Robert McRackan
13378a482d delete unused screenshots 2019-12-23 10:41:55 -05:00
rmcrackan
352b498c23 Update README.md 2019-12-23 10:41:32 -05:00
rmcrackan
3a652cfb70 Update README.md 2019-12-23 10:38:32 -05:00
Robert McRackan
93e9ce31ba update readme images 2019-12-23 10:37:54 -05:00
Robert McRackan
69ed7767b2 update readme screenshots 2019-12-23 10:29:40 -05:00
Robert McRackan
6fcaa8d551 Improved pdf icons 2019-12-23 10:09:28 -05:00
Robert McRackan
15ece43463 Update icon. Add glow so it can be seen on black background. Not as attractive, but no longer invisible 2019-12-23 09:27:11 -05:00
Robert McRackan
25f5f0ed14 Change liberate text buttons to images 2019-12-20 16:37:50 -05:00
Robert McRackan
de66e5b405 Clean up for 3.1 beta 11 2019-12-18 14:27:27 -05:00
Robert McRackan
73c671b7c0 Removed temp fix of hard-coded logging level. Now is correctly set via config file 2019-12-18 10:50:46 -05:00
Robert McRackan
4994684690 Improved configuration management 2019-12-17 10:09:33 -05:00
Robert McRackan
6c757773f7 Minor string change 2019-12-16 13:40:54 -05:00
Robert McRackan
2d0af587d5 Add new settings options to main form 2019-12-16 11:18:50 -05:00
Robert McRackan
c7891dc448 2 bug fixes which prevented saving to db 2019-12-16 10:32:38 -05:00
Robert McRackan
95ae8335a1 Improved settings 2019-12-13 16:11:55 -05:00
Robert McRackan
2fa5170f28 Pluralize LibationWinForms 2019-12-10 10:45:14 -05:00
Robert McRackan
123a32ff9b Move application logic from view to Launcher 2019-12-10 10:33:51 -05:00
Robert McRackan
41620352e8 Add appsettings.json to main application. This will overwrite the one previously inherited from DataLayer 2019-12-09 15:24:25 -05:00
Robert McRackan
54eea8ddae DataLayer appsettings.json is only used for Migrations. It is not directly used by application. Make notes 2019-12-09 14:51:33 -05:00
Robert McRackan
bcc237c693 sqlite db file should live in LibationFiles dir, not in app dir 2019-12-09 14:44:46 -05:00
Robert McRackan
65dc273e12 update release notes 2019-12-06 10:23:05 -05:00
Robert McRackan
7bb4853903 Clicking Liberate button on a liberated item navigates to that audio file 2019-12-06 09:53:07 -05:00
Robert McRackan
f9917d4064 edit comments 2019-12-05 16:14:39 -05:00
Robert McRackan
0f9f0d9eae New feature: liberate individual book 2019-12-05 15:55:46 -05:00
Robert McRackan
498aeaac3a Change "Download Status" column to "Liberate" button column. Displays text status. No functionality added yet 2019-12-05 12:39:38 -05:00
Robert McRackan
9534969c2d Series should sort irrespective of initial the/a/an (like Title already does) 2019-12-04 13:32:25 -05:00
Robert McRackan
b120bb8a66 Replace custom FileLogger with Serilog 2019-12-04 09:58:31 -05:00
Robert McRackan
f8a51f0882 Upgrade to Core 3.1 2019-12-03 16:47:53 -05:00
Robert McRackan
7529fdf878 Add logging 2019-12-02 15:14:19 -05:00
Robert McRackan
f1aacd92ad Bugfix: decrypt file conflict 2019-12-02 14:39:46 -05:00
Robert McRackan
b1b426427c Bugfix: initial bottom counts can throw error when a book was moved since Libation was last run 2019-11-27 16:57:35 -05:00
Robert McRackan
0683e5f55b Optimize tag persistence 2019-11-27 15:36:34 -05:00
Robert McRackan
5c81441f83 Bugfix: decrypt book with no author 2019-11-27 09:27:43 -05:00
Robert McRackan
57bc74cd23 Improved logging for file decrypt 2019-11-26 13:13:16 -05:00
Robert McRackan
1cecd4ba2e Improved logging. Updated nuget packages 2019-11-26 10:42:38 -05:00
Robert McRackan
7a4bd639fb Add notes for v3.1-beta.5 2019-11-25 14:12:07 -05:00
Robert McRackan
87e6a46808 remove db file 2019-11-25 14:09:53 -05:00
Robert McRackan
a2e30df51f Improved importing 2019-11-25 13:45:29 -05:00
Robert McRackan
c8e759c067 update notes 2019-11-24 21:47:03 -05:00
Robert McRackan
6c9074169a Added beta-specific logging 2019-11-24 21:45:35 -05:00
Robert McRackan
1375da2065 Improved performance calculating "liberated" status 2019-11-21 23:07:06 -05:00
Robert McRackan
d5d72a13f6 Login dialogs can get lost. Show on task bar 2019-11-20 13:00:13 -05:00
Robert McRackan
a1ba324166 Has PDFs => Has PDF 2019-11-19 13:34:38 -05:00
Robert McRackan
b0139c47be live update newly downloaded and cached images 2019-11-19 11:22:41 -05:00
Robert McRackan
80b0ef600d Better ToString for DataLayer objects 2019-11-19 09:54:42 -05:00
Robert McRackan
f3128b562d Fix performance issues, esp regarding saving tags 2019-11-18 14:37:17 -05:00
Robert McRackan
6734dec55c remove TODO from git 2019-11-16 21:30:52 -05:00
Robert McRackan
b9314ac678 Added validation and error handling
BETA READY
2019-11-15 22:43:04 -05:00
359 changed files with 14744 additions and 11923 deletions

74
.gitignore vendored
View File

@@ -4,6 +4,7 @@
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
@@ -12,6 +13,9 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
@@ -19,10 +23,15 @@
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
@@ -36,9 +45,10 @@ Generated\ Files/
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
@@ -52,7 +62,9 @@ BenchmarkDotNet.Artifacts/
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
@@ -60,7 +72,7 @@ StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*_h.h
*.ilk
*.meta
*.obj
@@ -77,6 +89,7 @@ StyleCopReport.xml
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
@@ -119,9 +132,6 @@ _ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
@@ -132,6 +142,11 @@ _TeamCity*
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
@@ -179,6 +194,8 @@ PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
@@ -203,12 +220,14 @@ BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
!?*.[Cc]ache/
# Others
ClientBin/
@@ -221,7 +240,7 @@ ClientBin/
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
@@ -252,6 +271,9 @@ ServiceFabricBackup/
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
@@ -287,12 +309,8 @@ paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
@@ -317,7 +335,7 @@ __pycache__/
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
@@ -326,5 +344,29 @@ ASALocalRun/
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
### manually ignored files
# Windows shortcuts
*.lnk
/__TODO.txt
/DataLayer/LibationContext.db

View File

@@ -1,123 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="taglib-sharp">
<HintPath>lib\taglib-sharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="..\..\..\..\..\..\Dinah%2527s folder\coding\_NET\Visual Studio 2019\Libation\AaxDecrypter\UNTESTED\BytesCrackerLib\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\AAXClean\AAXClean.csproj" />
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="BytesCrackerLib\alglib1.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_0_10000x789935_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_1_10000x791425_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_2_10000x790991_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_3_10000x792120_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_4_10000x790743_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_5_10000x790568_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_6_10000x791458_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_7_10000x791707_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_8_10000x790202_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\audible_byte#4-4_9_10000x791022_0.rtc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\ffmpeg.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\ffprobe.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="BytesCrackerLib\rcrack.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\AtomicParsley.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avcodec-57.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avdevice-57.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avfilter-6.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avformat-57.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avutil-55.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\cygcrypto-1.0.0.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\cyggcc_s-1.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\cygmp4v2-2.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\cygstdc++-6.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\cygwin1.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\cygz.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\ffmpeg.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\ffprobe.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\mp4trackdump.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\postproc-54.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\swresample-2.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\swscale-4.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\taglib-sharp.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,228 @@
using AAXClean;
using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO;
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);
public string OutputFileName { get; private set; }
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();
headers.Add("User-Agent", downloadLicense.UserAgent);
NetworkFileStream 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

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

59
AaxDecrypter/Cue.cs Normal file
View File

@@ -0,0 +1,59 @@
using System;
using System.IO;
using System.Text;
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public static class Cue
{
public static string CreateContents(string filePath, ChapterInfo chapters)
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
var trackCount = 0;
foreach (var c in chapters.Chapters)
{
trackCount++;
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)c.StartOffset.TotalMinutes}:{c.StartOffset:ss\\:ff}");
}
return stringBuilder.ToString();
}
public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath)
=> UpdateFileName(cueFileInfo.FullName, audioFilePath);
public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo)
=> UpdateFileName(cueFilePath, audioFileInfo.FullName);
public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo)
=> UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName);
public static void UpdateFileName(string cueFilePath, string audioFilePath)
{
var cueContents = File.ReadAllLines(cueFilePath);
for (var i = 0; i < cueContents.Length; i++)
{
var line = cueContents[i];
if (!line.Trim().StartsWith("FILE") || !line.Contains(" "))
continue;
var fileTypeBegins = line.LastIndexOf(" ") + 1;
cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]);
break;
}
File.WriteAllLines(cueFilePath, cueContents);
}
private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}";
}
}

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -0,0 +1,27 @@
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;
}
}
}

54
AaxDecrypter/NFO.cs Normal file
View File

@@ -0,0 +1,54 @@
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

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

View File

@@ -0,0 +1,23 @@
using Dinah.Core.IO;
using Newtonsoft.Json;
namespace AaxDecrypter
{
internal class NetworkFileStreamPersister : JsonFilePersister<NetworkFileStream>
{
/// <summary>Alias for Target </summary>
public NetworkFileStream NetworkFileStream => Target;
/// <summary>uses path. create file if doesn't yet exist</summary>
public NetworkFileStreamPersister(NetworkFileStream networkFileStream, string path, string jsonPath = null)
: base(networkFileStream, path, jsonPath) { }
/// <summary>load from existing file</summary>
public NetworkFileStreamPersister(string path, string jsonPath = null)
: base(path, jsonPath) { }
protected override JsonSerializerSettings GetSerializerSettings() => NetworkFileStream.GetJsonSerializerSettings();
}
}

View File

@@ -1,355 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO;
using Dinah.Core.StepRunner;
namespace AaxDecrypter
{
public interface ISimpleAaxToM4bConverter
{
event EventHandler<int> DecryptProgressUpdate;
bool Run();
string AppName { get; set; }
string inputFileName { get; }
byte[] coverBytes { get; }
string outDir { get; }
string outputFileName { get; }
Chapters chapters { get; }
Tags tags { get; }
EncodingInfo encodingInfo { get; }
void SetOutputFilename(string outFileName);
}
public interface IAdvancedAaxToM4bConverter : ISimpleAaxToM4bConverter
{
bool Step1_CreateDir();
bool Step2_DecryptAax();
bool Step3_Chapterize();
bool Step4_InsertCoverArt();
bool Step5_Cleanup();
bool Step6_AddTags();
bool End_CreateCue();
bool End_CreateNfo();
}
/// <summary>full c# app. integrated logging. no UI</summary>
public class AaxToM4bConverter : IAdvancedAaxToM4bConverter
{
public event EventHandler<int> DecryptProgressUpdate;
public string inputFileName { get; }
public string decryptKey { get; private set; }
private StepSequence steps { get; }
public byte[] coverBytes { get; private set; }
public string AppName { get; set; } = nameof(AaxToM4bConverter);
public string outDir { get; private set; }
public string outputFileName { get; private set; }
public Chapters chapters { get; private set; }
public Tags tags { get; private set; }
public EncodingInfo encodingInfo { get; private set; }
public static async Task<AaxToM4bConverter> CreateAsync(string inputFile, string decryptKey)
{
var converter = new AaxToM4bConverter(inputFile, decryptKey);
await converter.prelimProcessing();
converter.printPrelim();
return converter;
}
private AaxToM4bConverter(string inputFile, string decryptKey)
{
if (string.IsNullOrWhiteSpace(inputFile)) throw new ArgumentNullException(nameof(inputFile), "Input file may not be null or whitespace");
if (!File.Exists(inputFile)) throw new ArgumentNullException(nameof(inputFile), "File does not exist");
steps = new StepSequence
{
Name = "Convert Aax To M4b",
["Step 1: Create Dir"] = Step1_CreateDir,
["Step 2: Decrypt Aax"] = Step2_DecryptAax,
["Step 3: Chapterize and tag"] = Step3_Chapterize,
["Step 4: Insert Cover Art"] = Step4_InsertCoverArt,
["Step 5: Cleanup"] = Step5_Cleanup,
["Step 6: Add Tags"] = Step6_AddTags,
["End: Create Cue"] = End_CreateCue,
["End: Create Nfo"] = End_CreateNfo
};
this.inputFileName = inputFile;
this.decryptKey = decryptKey;
}
private async Task prelimProcessing()
{
this.tags = new Tags(this.inputFileName);
this.encodingInfo = new EncodingInfo(this.inputFileName);
this.chapters = new Chapters(this.inputFileName, this.tags.duration.TotalSeconds);
var defaultFilename = Path.Combine(
Path.GetDirectoryName(this.inputFileName),
getASCIITag(this.tags.author),
getASCIITag(this.tags.title) + ".m4b"
);
SetOutputFilename(defaultFilename);
await Task.Run(() => saveCover(inputFileName));
}
private string getASCIITag(string property)
{
foreach (char ch in new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()))
property = property.Replace(ch.ToString(), "");
return property;
}
private void saveCover(string aaxFile)
{
using var file = TagLib.File.Create(aaxFile, "audio/mp4", TagLib.ReadStyle.Average);
this.coverBytes = file.Tag.Pictures[0].Data.Data;
}
private void printPrelim()
{
Console.WriteLine("Audible Book ID = " + tags.id);
Console.WriteLine("Book: " + tags.title);
Console.WriteLine("Author: " + tags.author);
Console.WriteLine("Narrator: " + tags.narrator);
Console.WriteLine("Year: " + tags.year);
Console.WriteLine("Total Time: "
+ tags.duration.GetTotalTimeFormatted()
+ " in " + chapters.Count() + " chapters");
Console.WriteLine("WARNING-Source is "
+ encodingInfo.originalBitrate + " kbits @ "
+ encodingInfo.sampleRate + "Hz, "
+ encodingInfo.channels + " channels");
}
public bool Run()
{
var (IsSuccess, Elapsed) = steps.Run();
if (!IsSuccess)
{
Console.WriteLine("WARNING-Conversion failed");
return false;
}
var speedup = (int)(tags.duration.TotalSeconds / (long)Elapsed.TotalSeconds);
Console.WriteLine("Speedup is " + speedup + "x realtime.");
Console.WriteLine("Done");
return true;
}
public void SetOutputFilename(string outFileName)
{
this.outputFileName = outFileName;
if (Path.GetExtension(this.outputFileName) != ".m4b")
this.outputFileName = outputFileWithNewExt(".m4b");
this.outDir = Path.GetDirectoryName(this.outputFileName);
}
private string outputFileWithNewExt(string extension)
=> Path.Combine(this.outDir, Path.GetFileNameWithoutExtension(this.outputFileName) + '.' + extension.Trim('.'));
public bool Step1_CreateDir()
{
ProcessRunner.WorkingDir = this.outDir;
Directory.CreateDirectory(this.outDir);
return true;
}
public bool Step2_DecryptAax()
{
DecryptProgressUpdate?.Invoke(this, 0);
var tempRipFile = Path.Combine(this.outDir, "funny.aac");
var fail = "WARNING-Decrypt failure. ";
int returnCode;
if (string.IsNullOrWhiteSpace(decryptKey))
{
returnCode = getKey_decrypt(tempRipFile);
}
else
{
returnCode = decrypt(tempRipFile);
if (returnCode == -99)
{
Console.WriteLine($"{fail}Incorrect decrypt key: {decryptKey}");
this.decryptKey = null;
returnCode = getKey_decrypt(tempRipFile);
}
}
if (returnCode == 100)
Console.WriteLine($"{fail}Thread completed without changing return code. This shouldn't be possible");
else if (returnCode == 0)
{
// success!
FileExt.SafeMove(tempRipFile, outputFileWithNewExt(".mp4"));
DecryptProgressUpdate?.Invoke(this, 100);
return true;
}
else if (returnCode == -99)
Console.WriteLine($"{fail}Incorrect decrypt key: {decryptKey}");
else // any other returnCode
Console.WriteLine($"{fail}Unknown failure code: {returnCode}");
FileExt.SafeDelete(tempRipFile);
DecryptProgressUpdate?.Invoke(this, 0);
return false;
}
private int getKey_decrypt(string tempRipFile)
{
getKey();
return decrypt(tempRipFile);
}
private void getKey()
{
Console.WriteLine("Discovering decrypt key");
Console.WriteLine("Getting file hash");
var checksum = BytesCracker.GetChecksum(inputFileName);
Console.WriteLine("File hash calculated: " + checksum);
Console.WriteLine("Cracking activation bytes");
var activation_bytes = BytesCracker.GetActivationBytes(checksum);
this.decryptKey = activation_bytes;
Console.WriteLine("Activation bytes cracked. Decrypt key: " + activation_bytes);
}
private int decrypt(string tempRipFile)
{
FileExt.SafeDelete(tempRipFile);
Console.WriteLine("Decrypting with key " + decryptKey);
var returnCode = 100;
var thread = new Thread(() => returnCode = this.ngDecrypt());
thread.Start();
double fileLen = new FileInfo(this.inputFileName).Length;
while (thread.IsAlive && returnCode == 100)
{
Thread.Sleep(500);
if (File.Exists(tempRipFile))
{
double tempLen = new FileInfo(tempRipFile).Length;
var percentProgress = tempLen / fileLen * 100.0;
DecryptProgressUpdate?.Invoke(this, (int)percentProgress);
}
}
return returnCode;
}
private int ngDecrypt()
{
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.mp4trackdumpPath,
Arguments = "-c " + this.encodingInfo.channels + " -r " + this.encodingInfo.sampleRate + " \"" + this.inputFileName + "\""
};
info.EnvironmentVariables["VARIABLE"] = decryptKey;
var (output, exitCode) = info.RunHidden();
// bad checksum -- bad decrypt key
if (output.Contains("checksums mismatch, aborting!"))
return -99;
return exitCode;
}
// temp file names for steps 3, 4, 5
string tempChapsPath => Path.Combine(this.outDir, "tempChaps.mp4");
string mp4_file => outputFileWithNewExt(".mp4");
string ff_txt_file => mp4_file + ".ff.txt";
public bool Step3_Chapterize()
{
string str1 = "";
if (this.chapters.FirstChapterStart != 0.0)
{
str1 = " -ss " + this.chapters.FirstChapterStart.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + (this.chapters.LastChapterStart - 1.0).ToString("0.000", CultureInfo.InvariantCulture) + " ";
}
string ffmpegTags = this.tags.GenerateFfmpegTags();
string ffmpegChapters = this.chapters.GenerateFfmpegChapters();
File.WriteAllText(ff_txt_file, ffmpegTags + ffmpegChapters);
var tagAndChapterInfo = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffmpegPath,
Arguments = "-y -i \"" + mp4_file + "\" -f ffmetadata -i \"" + ff_txt_file + "\" -map_metadata 1 -bsf:a aac_adtstoasc -c:a copy" + str1 + " -map 0 \"" + tempChapsPath + "\""
};
tagAndChapterInfo.RunHidden();
return true;
}
public bool Step4_InsertCoverArt()
{
// save cover image as temp file
var coverPath = Path.Combine(this.outDir, "cover-" + Guid.NewGuid() + ".jpg");
FileExt.CreateFile(coverPath, this.coverBytes);
var insertCoverArtInfo = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.atomicParsleyPath,
Arguments = "\"" + tempChapsPath + "\" --encodingTool \"" + AppName + "\" --artwork \"" + coverPath + "\" --overWrite"
};
insertCoverArtInfo.RunHidden();
// delete temp file
FileExt.SafeDelete(coverPath);
return true;
}
public bool Step5_Cleanup()
{
FileExt.SafeDelete(mp4_file);
FileExt.SafeDelete(ff_txt_file);
FileExt.SafeMove(tempChapsPath, this.outputFileName);
return true;
}
public bool Step6_AddTags()
{
this.tags.AddAppleTags(this.outputFileName);
return true;
}
public bool End_CreateCue()
{
File.WriteAllText(outputFileWithNewExt(".cue"), this.chapters.GetCuefromChapters(Path.GetFileName(this.outputFileName)));
return true;
}
public bool End_CreateNfo()
{
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateNfoContents(AppName, this.tags, this.encodingInfo, this.chapters));
return true;
}
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Dinah.Core;
using Dinah.Core.Diagnostics;
namespace AaxDecrypter
{
public static class BytesCracker
{
public static string GetChecksum(string aaxPath)
{
var info = new ProcessStartInfo
{
FileName = BytesCrackerSupportLibraries.ffprobePath,
Arguments = aaxPath.SurroundWithQuotes(),
WorkingDirectory = Directory.GetCurrentDirectory()
};
// checksum is in the debug info. ffprobe's debug info is written to stderr, not stdout
var readErrorOutput = true;
var ffprobeStderr = info.RunHidden(readErrorOutput).Output;
// example checksum line:
// ... [aax] file checksum == 0c527840c4f18517157eb0b4f9d6f9317ce60cd1
var checksum = ffprobeStderr.ExtractString("file checksum == ", 40);
return checksum;
}
/// <summary>use checksum to get activation bytes. activation bytes are unique per audible customer. only have to do this 1x/customer</summary>
public static string GetActivationBytes(string checksum)
{
var info = new ProcessStartInfo
{
FileName = BytesCrackerSupportLibraries.rcrackPath,
Arguments = @". -h " + checksum,
WorkingDirectory = Directory.GetCurrentDirectory()
};
var rcrackStdout = info.RunHidden().Output;
// example result
// 0c527840c4f18517157eb0b4f9d6f9317ce60cd1 \xbd\x89X\x09 hex:bd895809
var activation_bytes = rcrackStdout.ExtractString("hex:", 8);
return activation_bytes;
}
}
}

View File

@@ -1,28 +0,0 @@
using System.IO;
namespace AaxDecrypter
{
public static class BytesCrackerSupportLibraries
{
// GetActivationBytes dependencies
// rcrack.exe
// alglib1.dll
// RainbowCrack files to recover your own Audible activation data (activation_bytes) in an offline manner
// audible_byte#4-4_0_10000x789935_0.rtc
// audible_byte#4-4_1_10000x791425_0.rtc
// audible_byte#4-4_2_10000x790991_0.rtc
// audible_byte#4-4_3_10000x792120_0.rtc
// audible_byte#4-4_4_10000x790743_0.rtc
// audible_byte#4-4_5_10000x790568_0.rtc
// audible_byte#4-4_6_10000x791458_0.rtc
// audible_byte#4-4_7_10000x791707_0.rtc
// audible_byte#4-4_8_10000x790202_0.rtc
// audible_byte#4-4_9_10000x791022_0.rtc
private static string appPath_ { get; } = Path.GetDirectoryName(Dinah.Core.Exe.FileLocationOnDisk);
private static string bytesCrackerLib_ { get; } = Path.Combine(appPath_, "BytesCrackerLib");
public static string ffprobePath { get; } = Path.Combine(bytesCrackerLib_, "ffprobe.exe");
public static string rcrackPath { get; } = Path.Combine(bytesCrackerLib_, "rcrack.exe");
}
}

View File

@@ -1,95 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using Dinah.Core.Diagnostics;
namespace AaxDecrypter
{
public class Chapters
{
private List<double> markers { get; }
public double FirstChapterStart => markers[0];
public double LastChapterStart => markers[markers.Count - 1];
public Chapters(string file, double totalTime)
{
this.markers = getAAXChapters(file);
// add end time
this.markers.Add(totalTime);
}
private static List<double> getAAXChapters(string file)
{
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffprobePath,
Arguments = "-loglevel panic -show_chapters -print_format xml \"" + file + "\""
};
var xml = info.RunHidden().Output;
var xmlDocument = new System.Xml.XmlDocument();
xmlDocument.LoadXml(xml);
var chapters = xmlDocument.SelectNodes("/ffprobe/chapters/chapter")
.Cast<System.Xml.XmlNode>()
.Select(xmlNode => double.Parse(xmlNode.Attributes["start_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture))
.ToList();
return chapters;
}
// subtract 1 b/c end time marker is a real entry but isn't a real chapter
public int Count() => this.markers.Count - 1;
public string GetCuefromChapters(string fileName)
{
var stringBuilder = new StringBuilder();
if (fileName != "")
{
stringBuilder.Append("FILE \"" + fileName + "\" MP4\n");
}
for (var i = 0; i < Count(); i++)
{
var chapter = i + 1;
var timeSpan = TimeSpan.FromSeconds(this.markers[i]);
var minutes = Math.Floor(timeSpan.TotalMinutes).ToString();
var seconds = timeSpan.Seconds.ToString("D2");
var milliseconds = (timeSpan.Milliseconds / 10).ToString("D2");
string str = minutes + ":" + seconds + ":" + milliseconds;
stringBuilder.Append("TRACK " + chapter + " AUDIO\n");
stringBuilder.Append(" TITLE \"Chapter " + chapter.ToString("D2") + "\"\n");
stringBuilder.Append(" INDEX 01 " + str + "\n");
}
return stringBuilder.ToString();
}
public string GenerateFfmpegChapters()
{
var stringBuilder = new StringBuilder();
for (var i = 0; i < Count(); i++)
{
var chapter = i + 1;
var start = this.markers[i] * 1000.0;
var end = this.markers[i + 1] * 1000.0;
var chapterName = chapter.ToString("D3");
stringBuilder.Append("[CHAPTER]\n");
stringBuilder.Append("TIMEBASE=1/1000\n");
stringBuilder.Append("START=" + start + "\n");
stringBuilder.Append("END=" + end + "\n");
stringBuilder.Append("title=" + chapterName + "\n");
}
return stringBuilder.ToString();
}
}
}

View File

@@ -1,21 +0,0 @@
using System.IO;
namespace AaxDecrypter
{
public static class DecryptSupportLibraries
{
// OTHER EXTERNAL DEPENDENCIES
// ffprobe has these pre-req.s as I'm using it:
// avcodec-57.dll, avdevice-57.dll, avfilter-6.dll, avformat-57.dll, avutil-55.dll, postproc-54.dll, swresample-2.dll, swscale-4.dll, taglib-sharp.dll
//
// something else needs the cygwin files (cyg*.dll)
private static string appPath_ { get; } = Path.GetDirectoryName(Dinah.Core.Exe.FileLocationOnDisk);
private static string decryptLib_ { get; } = Path.Combine(appPath_, "DecryptLib");
public static string ffmpegPath { get; } = Path.Combine(decryptLib_, "ffmpeg.exe");
public static string ffprobePath { get; } = Path.Combine(decryptLib_, "ffprobe.exe");
public static string atomicParsleyPath { get; } = Path.Combine(decryptLib_, "AtomicParsley.exe");
public static string mp4trackdumpPath { get; } = Path.Combine(decryptLib_, "mp4trackdump.exe");
}
}

View File

@@ -1,41 +0,0 @@
using System;
using System.Diagnostics;
using Dinah.Core.Diagnostics;
namespace AaxDecrypter
{
public class EncodingInfo
{
public int sampleRate { get; } = 44100;
public int channels { get; } = 2;
public int originalBitrate { get; }
public EncodingInfo(string file)
{
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffprobePath,
Arguments = "-loglevel panic -show_streams -print_format flat \"" + file + "\""
};
var end = info.RunHidden().Output;
foreach (string str2 in end.Split('\n'))
{
string[] strArray = str2.Split('=');
switch (strArray[0])
{
case "streams.stream.0.channels":
this.channels = int.Parse(strArray[1].Replace("\"", "").TrimEnd('\r', '\n'));
break;
case "streams.stream.0.sample_rate":
this.sampleRate = int.Parse(strArray[1].Replace("\"", "").TrimEnd('\r', '\n'));
break;
case "streams.stream.0.bit_rate":
string s = strArray[1].Replace("\"", "").TrimEnd('\r', '\n');
this.originalBitrate = (int)Math.Round(double.Parse(s) / 1000.0, MidpointRounding.AwayFromZero);
break;
}
}
}
}
}

View File

@@ -1,56 +0,0 @@
namespace AaxDecrypter
{
public static class NFO
{
public static string CreateNfoContents(string ripper, Tags tags, EncodingInfo encodingInfo, Chapters chapters)
{
int _hours = (int)tags.duration.TotalHours;
string myDuration
= (_hours > 0 ? _hours + " hours, " : "")
+ tags.duration.Minutes + " minutes, "
+ tags.duration.Seconds + " seconds";
string str4
= "General Information\r\n"
+ "===================\r\n"
+ " Title: " + tags.title + "\r\n"
+ " Author: " + tags.author + "\r\n"
+ " Read By: " + tags.narrator + "\r\n"
+ " Copyright: " + tags.year + "\r\n"
+ " Audiobook Copyright: " + tags.year + "\r\n";
if (tags.genre != "")
{
str4 = str4 + " Genre: " + tags.genre + "\r\n";
}
string s
= str4
+ " Publisher: " + tags.publisher + "\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: " + encodingInfo.sampleRate + " Hz\r\n"
+ " Source Channels: " + encodingInfo.channels + "\r\n"
+ " Source Bitrate: " + encodingInfo.originalBitrate + " kbits\r\n"
+ "\r\n"
+ " Lossless Encode: Yes\r\n"
+ " Encoded Codec: AAC / M4B\r\n"
+ " Encoded Sample Rate: " + encodingInfo.sampleRate + " Hz\r\n"
+ " Encoded Channels: " + encodingInfo.channels + "\r\n"
+ " Encoded Bitrate: " + encodingInfo.originalBitrate + " kbits\r\n"
+ "\r\n"
+ " Ripper: " + ripper + "\r\n"
+ "\r\n"
+ "\r\n"
+ "Book Description\r\n"
+ "================\r\n"
+ tags.comments;
return s;
}
}
}

View File

@@ -1,74 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using TagLib;
using TagLib.Mpeg4;
using Dinah.Core;
namespace AaxDecrypter
{
public class Tags
{
public string title { get; }
public string album { get; }
public string author { get; }
public string comments { get; }
public string narrator { get; }
public string year { get; }
public string publisher { get; }
public string id { get; }
public string genre { get; }
public TimeSpan duration { get; }
public Tags(string file)
{
using TagLib.File tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
this.title = tagLibFile.Tag.Title.Replace(" (Unabridged)", "");
this.album = tagLibFile.Tag.Album.Replace(" (Unabridged)", "");
this.author = tagLibFile.Tag.FirstPerformer;
this.year = tagLibFile.Tag.Year.ToString();
this.comments = tagLibFile.Tag.Comment;
this.duration = tagLibFile.Properties.Duration;
this.genre = tagLibFile.Tag.FirstGenre;
var tag = tagLibFile.GetTag(TagTypes.Apple, true);
this.publisher = tag.Publisher;
this.narrator = string.IsNullOrWhiteSpace(tagLibFile.Tag.FirstComposer) ? tag.Narrator : tagLibFile.Tag.FirstComposer;
this.comments = !string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description;
this.id = tag.AudibleCDEK;
}
public void AddAppleTags(string file)
{
using var file1 = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
var tag = (AppleTag)file1.GetTag(TagTypes.Apple, true);
tag.Publisher = this.publisher;
tag.LongDescription = this.comments;
tag.Description = this.comments;
file1.Save();
}
public string GenerateFfmpegTags()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append(";FFMETADATA1\n");
stringBuilder.Append("major_brand=aax\n");
stringBuilder.Append("minor_version=1\n");
stringBuilder.Append("compatible_brands=aax M4B mp42isom\n");
stringBuilder.Append("date=" + this.year + "\n");
stringBuilder.Append("genre=" + this.genre + "\n");
stringBuilder.Append("title=" + this.title + "\n");
stringBuilder.Append("artist=" + this.author + "\n");
stringBuilder.Append("album=" + this.album + "\n");
stringBuilder.Append("composer=" + this.narrator + "\n");
stringBuilder.Append("comment=" + this.comments.Truncate(254) + "\n");
stringBuilder.Append("description=" + this.comments + "\n");
return stringBuilder.ToString();
}
}
}

View File

@@ -1,9 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<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" />

View File

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,123 @@
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
{
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;
}
public static int UpdateTags(this LibationContext context, Book book, string newTags)
{
try
{
book.UserDefinedItem.Tags = newTags;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
SearchEngineCommands.UpdateBookTags(book);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
throw;
}
}
}
}

View File

@@ -0,0 +1,268 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CsvHelper;
using CsvHelper.Configuration.Attributes;
using DataLayer;
using NPOI.XSSF.UserModel;
using Serilog;
namespace ApplicationServices
{
public class ExportDto
{
public static string GetName(string fieldName)
{
var property = typeof(ExportDto).GetProperty(fieldName);
var attribute = property.GetCustomAttributes(typeof(NameAttribute), true)[0];
var description = (NameAttribute)attribute;
var text = description.Names;
return text[0];
}
[Name("Account")]
public string Account { get; set; }
[Name("Date Added to library")]
public DateTime DateAdded { get; set; }
[Name("Audible Product Id")]
public string AudibleProductId { get; set; }
[Name("Locale")]
public string Locale { get; set; }
[Name("Title")]
public string Title { get; set; }
[Name("Authors")]
public string AuthorNames { get; set; }
[Name("Narrators")]
public string NarratorNames { get; set; }
[Name("Length In Minutes")]
public int LengthInMinutes { get; set; }
[Name("Publisher")]
public string Publisher { get; set; }
[Name("Pdf url")]
public string PdfUrl { get; set; }
[Name("Series Names")]
public string SeriesNames { get; set; }
[Name("Series Order")]
public string SeriesOrder { get; set; }
[Name("Community Rating: Overall")]
public float? CommunityRatingOverall { get; set; }
[Name("Community Rating: Performance")]
public float? CommunityRatingPerformance { get; set; }
[Name("Community Rating: Story")]
public float? CommunityRatingStory { get; set; }
[Name("Cover Id")]
public string PictureId { get; set; }
[Name("Is Abridged?")]
public bool IsAbridged { get; set; }
[Name("Date Published")]
public DateTime? DatePublished { get; set; }
[Name("Categories")]
public string CategoriesNames { get; set; }
[Name("My Rating: Overall")]
public float? MyRatingOverall { get; set; }
[Name("My Rating: Performance")]
public float? MyRatingPerformance { get; set; }
[Name("My Rating: Story")]
public float? MyRatingStory { get; set; }
[Name("My Libation Tags")]
public string MyLibationTags { get; set; }
}
public static class LibToDtos
{
public static List<ExportDto> ToDtos(this IEnumerable<LibraryBook> library)
=> library.Select(a => new ExportDto
{
Account = a.Account,
DateAdded = a.DateAdded,
AudibleProductId = a.Book.AudibleProductId,
Locale = a.Book.Locale,
Title = a.Book.Title,
AuthorNames = a.Book.AuthorNames,
NarratorNames = a.Book.NarratorNames,
LengthInMinutes = a.Book.LengthInMinutes,
Publisher = a.Book.Publisher,
PdfUrl = a.Book.Supplements?.FirstOrDefault()?.Url,
SeriesNames = a.Book.SeriesNames,
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Index} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
CommunityRatingOverall = a.Book.Rating?.OverallRating,
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating,
CommunityRatingStory = a.Book.Rating?.StoryRating,
PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.CategoriesNames.Any() ? a.Book.CategoriesNames.Aggregate((a, b) => $"{a}, {b}") : "",
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
MyLibationTags = a.Book.UserDefinedItem.Tags
}).ToList();
}
public static class LibraryExporter
{
public static void ToCsv(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
if (!dtos.Any())
return;
using var writer = new System.IO.StreamWriter(saveFilePath);
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
csv.WriteHeader(typeof(ExportDto));
csv.NextRecord();
csv.WriteRecords(dtos);
}
public static void ToJson(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
var json = Newtonsoft.Json.JsonConvert.SerializeObject(dtos, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(saveFilePath, json);
}
public static void ToXlsx(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Library");
var detailSubtotalFont = workbook.CreateFont();
detailSubtotalFont.IsBold = true;
var detailSubtotalCellStyle = workbook.CreateCellStyle();
detailSubtotalCellStyle.SetFont(detailSubtotalFont);
// headers
var rowIndex = 0;
var row = sheet.CreateRow(rowIndex);
var columns = new[] {
nameof (ExportDto.Account),
nameof (ExportDto.DateAdded),
nameof (ExportDto.AudibleProductId),
nameof (ExportDto.Locale),
nameof (ExportDto.Title),
nameof (ExportDto.AuthorNames),
nameof (ExportDto.NarratorNames),
nameof (ExportDto.LengthInMinutes),
nameof (ExportDto.Publisher),
nameof (ExportDto.PdfUrl),
nameof (ExportDto.SeriesNames),
nameof (ExportDto.SeriesOrder),
nameof (ExportDto.CommunityRatingOverall),
nameof (ExportDto.CommunityRatingPerformance),
nameof (ExportDto.CommunityRatingStory),
nameof (ExportDto.PictureId),
nameof (ExportDto.IsAbridged),
nameof (ExportDto.DatePublished),
nameof (ExportDto.CategoriesNames),
nameof (ExportDto.MyRatingOverall),
nameof (ExportDto.MyRatingPerformance),
nameof (ExportDto.MyRatingStory),
nameof (ExportDto.MyLibationTags)
};
var col = 0;
foreach (var c in columns)
{
var cell = row.CreateCell(col++);
var name = ExportDto.GetName(c);
cell.SetCellValue(name);
cell.CellStyle = detailSubtotalCellStyle;
}
var dateFormat = workbook.CreateDataFormat();
var dateStyle = workbook.CreateCellStyle();
dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
rowIndex++;
// Add data rows
foreach (var dto in dtos)
{
col = 0;
row = sheet.CreateRow(rowIndex);
row.CreateCell(col++).SetCellValue(dto.Account);
var dateAddedCell = row.CreateCell(col++);
dateAddedCell.CellStyle = dateStyle;
dateAddedCell.SetCellValue(dto.DateAdded);
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
row.CreateCell(col++).SetCellValue(dto.Locale);
row.CreateCell(col++).SetCellValue(dto.Title);
row.CreateCell(col++).SetCellValue(dto.AuthorNames);
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
row.CreateCell(col++).SetCellValue(dto.Publisher);
row.CreateCell(col++).SetCellValue(dto.PdfUrl);
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
col = createCell(row, col, dto.CommunityRatingOverall);
col = createCell(row, col, dto.CommunityRatingPerformance);
col = createCell(row, col, dto.CommunityRatingStory);
row.CreateCell(col++).SetCellValue(dto.PictureId);
row.CreateCell(col++).SetCellValue(dto.IsAbridged);
var datePubCell = row.CreateCell(col++);
datePubCell.CellStyle = dateStyle;
if (dto.DatePublished.HasValue)
datePubCell.SetCellValue(dto.DatePublished.Value);
else
datePubCell.SetCellValue("");
row.CreateCell(col++).SetCellValue(dto.CategoriesNames);
col = createCell(row, col, dto.MyRatingOverall);
col = createCell(row, col, dto.MyRatingPerformance);
col = createCell(row, col, dto.MyRatingStory);
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
rowIndex++;
}
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
workbook.Write(fileData);
}
private static int createCell(NPOI.SS.UserModel.IRow row, int col, float? nullableFloat)
{
if (nullableFloat.HasValue)
row.CreateCell(col++).SetCellValue(nullableFloat.Value);
else
row.CreateCell(col++).SetCellValue("");
return col;
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.IO;
using DataLayer;
using LibationSearchEngine;
namespace ApplicationServices
{
public static class SearchEngineCommands
{
public static void FullReIndex()
{
var engine = new SearchEngine(DbContexts.GetContext());
engine.CreateNewIndex();
}
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
e.Search(searchString)
);
public static void UpdateBookTags(Book book) => performSearchEngineAction_safe(e =>
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
);
public static void UpdateIsLiberated(Book book) => performSearchEngineAction_safe(e =>
e.UpdateIsLiberated(book.AudibleProductId)
);
private static void performSearchEngineAction_safe(Action<SearchEngine> action)
{
var engine = new SearchEngine(DbContexts.GetContext());
try
{
action(engine);
}
catch (FileNotFoundException)
{
FullReIndex();
action(engine);
}
}
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> action)
{
var engine = new SearchEngine(DbContexts.GetContext());
try
{
return action(engine);
}
catch (FileNotFoundException)
{
FullReIndex();
return action(engine);
}
}
}
}

View File

@@ -1,40 +0,0 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using DataLayer;
using DtoImporterService;
using InternalUtilities;
namespace ApplicationServices
{
public static class LibraryCommands
{
public static async Task<(int totalCount, int newCount)> IndexLibraryAsync(ILoginCallback callback)
{
var audibleApiActions = new AudibleApiActions();
var items = await audibleApiActions.GetAllLibraryItemsAsync(callback);
var totalCount = items.Count;
var libImporter = new LibraryImporter();
var newCount = await Task.Run(() => libImporter.Import(items));
await Task.Run(() => SearchEngineCommands.FullReIndex());
return (totalCount, newCount);
}
public static int IndexChangedTags(Book book)
{
// update disconnected entity
using var context = LibationContext.Create();
context.Update(book);
var qtyChanges = context.SaveChanges();
// this part is tags-specific
if (qtyChanges > 0)
SearchEngineCommands.UpdateBookTags(book);
return qtyChanges;
}
}
}

View File

@@ -1,43 +0,0 @@
using System.Threading.Tasks;
using DataLayer;
using LibationSearchEngine;
namespace ApplicationServices
{
public static class SearchEngineCommands
{
public static void FullReIndex()
{
var engine = new SearchEngine();
engine.CreateNewIndex();
}
public static SearchResultSet Search(string searchString)
{
var engine = new SearchEngine();
try
{
return engine.Search(searchString);
}
catch (System.IO.FileNotFoundException)
{
FullReIndex();
return engine.Search(searchString);
}
}
public static void UpdateBookTags(Book book)
{
var engine = new SearchEngine();
try
{
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
}
catch (System.IO.FileNotFoundException)
{
FullReIndex();
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
}
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;netstandard2.1</TargetFrameworks>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@@ -12,13 +12,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -32,7 +31,6 @@
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>

View File

@@ -26,6 +26,9 @@ namespace DataLayer
public string Description { get; private set; }
public int LengthInMinutes { get; private set; }
// immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
public string Locale { get; private set; }
// mutable
public string PictureId { get; set; }
@@ -61,12 +64,19 @@ namespace DataLayer
string title,
string description,
int lengthInMinutes,
IEnumerable<Contributor> authors)
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators,
Category category, string localeName)
{
// validate
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
var productId = audibleProductId.Id;
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
// assign as soon as possible. stuff below relies on this
AudibleProductId = productId;
Locale = localeName;
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
// non-ef-ctor init.s
@@ -75,24 +85,17 @@ namespace DataLayer
_seriesLink = new HashSet<SeriesBook>();
_supplements = new HashSet<Supplement>();
// since category/id is never null, nullity means it hasn't been loaded
CategoryId = Category.GetEmpty().CategoryId;
Category = category;
// simple assigns
AudibleProductId = productId;
Title = title;
Description = description;
LengthInMinutes = lengthInMinutes;
// assigns with biz logic
ReplaceAuthors(authors);
//ReplaceNarrators(narrators);
// import previously saved tags
// do this immediately. any save occurs before reloading tags will overwrite persistent tags with new blank entries; all old persisted tags will be lost
// if refactoring, DO NOT use "ProductId" before it's assigned to. to be safe, just use "productId"
UserDefinedItem = new UserDefinedItem(this) { Tags = FileManager.TagsPersistence.GetTags(productId) };
}
ReplaceNarrators(narrators);
}
#region contributors, authors, narrators
// use uninitialised backing fields - this means we can detect if the collection was loaded
@@ -124,16 +127,10 @@ namespace DataLayer
ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors));
// the edge cases of doing local-loaded vs remote-only got weird. just load it
if (_contributorsLink == null)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
if (!context.Entry(this).IsKeySet)
throw new InvalidOperationException("Could not add contributors");
if (_contributorsLink is null)
getEntry(context).Collection(s => s.ContributorsLink).Load();
context.Entry(this).Collection(s => s.ContributorsLink).Load();
}
var roleContributions = getContributions(role);
var roleContributions = getContributions(role);
var isIdentical = roleContributions.Select(c => c.Contributor).SequenceEqual(newContributors);
if (isIdentical)
return;
@@ -141,7 +138,8 @@ namespace DataLayer
_contributorsLink.RemoveWhere(bc => bc.Role == role);
addNewContributors(newContributors, role);
}
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
{
byte order = 0;
var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++));
@@ -156,6 +154,18 @@ namespace DataLayer
.ToList();
#endregion
private Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Book> getEntry(DbContext context)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
var entry = context.Entry(this);
if (!entry.IsKeySet)
throw new InvalidOperationException("Could not load a valid Book from database");
return entry;
}
#region series
private HashSet<SeriesBook> _seriesLink;
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList();
@@ -187,16 +197,10 @@ namespace DataLayer
// our add() is conditional upon what's already included in the collection.
// therefore if not loaded, a trip is required. might as well just load it
if (_seriesLink == null)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
if (!context.Entry(this).IsKeySet)
throw new InvalidOperationException("Could not add series");
if (_seriesLink is null)
getEntry(context).Collection(s => s.SeriesLink).Load();
context.Entry(this).Collection(s => s.SeriesLink).Load();
}
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
if (singleSeriesBook == null)
_seriesLink.Add(new SeriesBook(series, this, index));
else
@@ -207,13 +211,12 @@ namespace DataLayer
#region supplements
private HashSet<Supplement> _supplements;
public IEnumerable<Supplement> Supplements => _supplements?.ToList();
public bool HasPdfs => Supplements.Any();
public bool HasPdf => Supplements.Any();
public void AddSupplementDownloadUrl(string url)
{
// supplements are owned by Book, so no need to Load():
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner.
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
@@ -233,19 +236,18 @@ namespace DataLayer
}
public void UpdateCategory(Category category, DbContext context = null)
{
// since category is never null, nullity means it hasn't been loaded
if (Category != null || CategoryId == Category.GetEmpty().CategoryId)
{
Category = category;
return;
}
{
// since category is never null, nullity means it hasn't been loaded
if (Category is null)
getEntry(context).Reference(s => s.Category).Load();
if (context == null)
throw new Exception("need context");
context.Entry(this).Reference(s => s.Category).Load();
Category = category;
Category = category;
}
}
// needed for v3 => v4 upgrade
public void UpdateLocale(string localeName)
=> Locale ??= localeName;
public override string ToString() => $"[{AudibleProductId}] {Title}";
}
}

View File

@@ -23,5 +23,7 @@ namespace DataLayer
Role = role;
Order = order;
}
}
public override string ToString() => $"{Book} {Contributor} {Role} {Order}";
}
}

View File

@@ -18,8 +18,7 @@ namespace DataLayer
public class Category
{
// Empty is a special case. use private ctor w/o validation
public static Category GetEmpty() => new Category { CategoryId = -1, AudibleCategoryId = "", Name = "", ParentCategory = null };
public bool IsEmpty() => string.IsNullOrWhiteSpace(AudibleCategoryId) || string.IsNullOrWhiteSpace(Name) || ParentCategory == null;
public static Category GetEmpty() => new Category { CategoryId = -1, AudibleCategoryId = "", Name = "" };
internal int CategoryId { get; private set; }
public string AudibleCategoryId { get; private set; }
@@ -48,5 +47,7 @@ namespace DataLayer
if (parentCategory != null)
ParentCategory = parentCategory;
}
}
public override string ToString() => $"[{AudibleCategoryId}] {Name}";
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
namespace DataLayer
{
public class Contributor
{
// Empty is a special case. use private ctor w/o validation
public static Contributor GetEmpty() => new Contributor { ContributorId = -1, Name = "" };
// contributors search links are just name with url-encoding. space can be + or %20
// author search link: /search?searchAuthor=Robert+Bevan
// narrator search link: /search?searchNarrator=Robert+Bevan
// can also search multiples. concat with comma before url encode
// id.s
// ----
// https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2
// goes to summary page
// at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman
// some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears
// all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman
internal int ContributorId { get; private set; }
public string Name { get; private set; }
private HashSet<BookContributor> _booksLink;
public IEnumerable<BookContributor> BooksLink => _booksLink?.ToList();
public string AudibleContributorId { get; private set; }
private Contributor() { }
public Contributor(string name)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
_booksLink = new HashSet<BookContributor>();
Name = name;
}
public Contributor(string name, string audibleContributorId) : this(name)
{
// don't overwrite with null or whitespace but not an error
if (!string.IsNullOrWhiteSpace(audibleContributorId))
AudibleContributorId = audibleContributorId;
}
public override string ToString() => Name;
}
}

View File

@@ -0,0 +1,33 @@
using System;
using Dinah.Core;
namespace DataLayer
{
public class LibraryBook
{
internal int BookId { get; private set; }
public Book Book { get; private set; }
public DateTime DateAdded { get; private set; }
// immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
public string Account { get; private set; }
private LibraryBook() { }
public LibraryBook(Book book, DateTime dateAdded, string account)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
ArgumentValidator.EnsureNotNull(account, nameof(account));
Book = book;
DateAdded = dateAdded;
Account = account;
}
// needed for v3 => v4 upgrade
public void UpdateAccount(string account)
=> Account ??= account;
public override string ToString() => $"{DateAdded:d} {Book}";
}
}

View File

@@ -72,5 +72,7 @@ namespace DataLayer
return string.Join("\r\n", items);
}
}
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
}
}

View File

@@ -66,5 +66,7 @@ namespace DataLayer
if (_booksLink.SingleOrDefault(sb => sb.Book == book) == null)
_booksLink.Add(new SeriesBook(this, book, index));
}
}
public override string ToString() => Name;
}
}

View File

@@ -34,5 +34,7 @@ namespace DataLayer
if (index.HasValue)
Index = index.Value;
}
}
public override string ToString() => $"Series={Series} Book={Book}";
}
}

View File

@@ -20,5 +20,7 @@ namespace DataLayer
Book = book;
Url = url;
}
}
public override string ToString() => $"{Book} {Url.Substring(Url.Length - 4)}";
}
}

View File

@@ -13,29 +13,36 @@ namespace DataLayer
private UserDefinedItem() { }
internal UserDefinedItem(Book book)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
Book = book;
}
// import previously saved tags
ArgumentValidator.EnsureNotNullOrWhiteSpace(book.AudibleProductId, nameof(book.AudibleProductId));
Tags = FileManager.TagsPersistence.GetTags(book.AudibleProductId);
}
private string _tags = "";
public string Tags
{
get => _tags;
set => _tags = sanitize(value);
}
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
// only legal chars are letters numbers underscores and separating whitespace
//
// technically, the only char.s which aren't easily supported are \ [ ]
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
// it's easy to expand whitelist as needed
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
//
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
// full list of characters which must be escaped:
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
static Regex regex = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
}
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
// only legal chars are letters numbers underscores and separating whitespace
//
// technically, the only char.s which aren't easily supported are \ [ ]
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
// it's easy to expand whitelist as needed
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
//
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
// full list of characters which must be escaped:
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
static Regex regex { get; } = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
private static string sanitize(string input)
{
if (string.IsNullOrWhiteSpace(input))
@@ -63,8 +70,6 @@ namespace DataLayer
return string.Join(" ", unique);
}
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
#endregion
// owned: not an optional one-to-one
@@ -73,5 +78,7 @@ namespace DataLayer
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
=> Rating.Update(overallRating, performanceRating, storyRating);
}
public override string ToString() => $"{Book} {Rating} {Tags}";
}
}

View File

@@ -25,10 +25,10 @@ namespace DataLayer
public DbSet<Series> Series { get; private set; }
public DbSet<Category> Categories { get; private set; }
public static LibationContext Create()
public static LibationContext Create(string connectionString)
{
var factory = new LibationContextFactory();
var context = factory.Create();
var context = factory.Create(connectionString);
return context;
}
@@ -56,12 +56,17 @@ namespace DataLayer
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
modelBuilder.ApplyConfiguration(new CategoryConfig());
// seeds go here. examples in scratch pad
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs
modelBuilder
.Entity<Category>()
.HasData(Category.GetEmpty());
.HasData(Category.GetEmpty());
modelBuilder
.Entity<Contributor>()
.HasData(Contributor.GetEmpty());
// views are now supported via "query types" (instead of "entity types"): https://docs.microsoft.com/en-us/ef/core/modeling/query-types
}
}
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types
}
}
}

View File

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

View File

@@ -3,54 +3,50 @@ using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20191115193402_Fresh")]
[Migration("20191125182309_Fresh")]
partial class Fresh
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasAnnotation("ProductVersion", "3.0.0");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("datetime2");
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("bit");
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<string>("PictureId")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.HasKey("BookId");
@@ -64,16 +60,16 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("tinyint");
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
@@ -88,17 +84,16 @@ namespace DataLayer.Migrations
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
@@ -121,29 +116,35 @@ namespace DataLayer.Migrations
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleAuthorId")
.HasColumnType("nvarchar(max)");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<DateTime>("DateAdded")
.HasColumnType("datetime2");
.HasColumnType("TEXT");
b.HasKey("BookId");
@@ -154,14 +155,13 @@ namespace DataLayer.Migrations
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.HasKey("SeriesId");
@@ -173,13 +173,13 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("real");
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
@@ -201,18 +201,16 @@ namespace DataLayer.Migrations
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.HasKey("BookId");
@@ -226,14 +224,13 @@ namespace DataLayer.Migrations
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
@@ -248,10 +245,10 @@ namespace DataLayer.Migrations
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b1.HasKey("BookId");
@@ -263,16 +260,16 @@ namespace DataLayer.Migrations
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");

View File

@@ -12,7 +12,7 @@ namespace DataLayer.Migrations
columns: table => new
{
CategoryId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
AudibleCategoryId = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
ParentCategoryCategoryId = table.Column<int>(nullable: true)
@@ -33,9 +33,9 @@ namespace DataLayer.Migrations
columns: table => new
{
ContributorId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(nullable: true),
AudibleAuthorId = table.Column<string>(nullable: true)
AudibleContributorId = table.Column<string>(nullable: true)
},
constraints: table =>
{
@@ -47,7 +47,7 @@ namespace DataLayer.Migrations
columns: table => new
{
SeriesId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
AudibleSeriesId = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true)
},
@@ -61,7 +61,7 @@ namespace DataLayer.Migrations
columns: table => new
{
BookId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
AudibleProductId = table.Column<string>(nullable: true),
Title = table.Column<string>(nullable: true),
Description = table.Column<string>(nullable: true),
@@ -159,7 +159,7 @@ namespace DataLayer.Migrations
columns: table => new
{
SupplementId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
BookId = table.Column<int>(nullable: false),
Url = table.Column<string>(nullable: true)
},
@@ -200,6 +200,11 @@ namespace DataLayer.Migrations
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
values: new object[] { -1, "", "", null });
migrationBuilder.InsertData(
table: "Contributors",
columns: new[] { "ContributorId", "AudibleContributorId", "Name" },
values: new object[] { -1, null, "" });
migrationBuilder.CreateIndex(
name: "IX_BookContributor_BookId",
table: "BookContributor",

View File

@@ -0,0 +1,338 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20200812152646_AddLocaleAndAccount")]
partial class AddLocaleAndAccount
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.7");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class AddLocaleAndAccount : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Account",
table: "Library",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Locale",
table: "Books",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Account",
table: "Library");
migrationBuilder.DropColumn(
name: "Locale",
table: "Books");
}
}
}

View File

@@ -0,0 +1,387 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20210619030017_AddAaxcDecryptionKeys")]
partial class AddAaxcDecryptionKeys
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.5");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleIV")
.HasColumnType("TEXT");
b.Property<string>("AudibleKey")
.HasColumnType("TEXT");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Category");
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class AddAaxcDecryptionKeys : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AudibleIV",
table: "Books",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "AudibleKey",
table: "Books",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AudibleIV",
table: "Books");
migrationBuilder.DropColumn(
name: "AudibleKey",
table: "Books");
}
}
}

View File

@@ -0,0 +1,381 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20210622205558_RemoveAaxcDecryptionKeys")]
partial class RemoveAaxcDecryptionKeys
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.5");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Category");
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class RemoveAaxcDecryptionKeys : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AudibleIV",
table: "Books");
migrationBuilder.DropColumn(
name: "AudibleKey",
table: "Books");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AudibleIV",
table: "Books",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "AudibleKey",
table: "Books",
type: "TEXT",
nullable: true);
}
}
}

View File

@@ -3,7 +3,6 @@ using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
@@ -15,40 +14,40 @@ namespace DataLayer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasAnnotation("ProductVersion", "5.0.5");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("datetime2");
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("bit");
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.HasKey("BookId");
@@ -62,16 +61,16 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("tinyint");
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
@@ -86,17 +85,16 @@ namespace DataLayer.Migrations
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
@@ -119,29 +117,38 @@ namespace DataLayer.Migrations
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleAuthorId")
.HasColumnType("nvarchar(max)");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("datetime2");
.HasColumnType("TEXT");
b.HasKey("BookId");
@@ -152,14 +159,13 @@ namespace DataLayer.Migrations
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.HasKey("SeriesId");
@@ -171,13 +177,13 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("real");
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
@@ -199,18 +205,16 @@ namespace DataLayer.Migrations
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.HasKey("BookId");
@@ -224,14 +228,13 @@ namespace DataLayer.Migrations
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
@@ -241,15 +244,17 @@ namespace DataLayer.Migrations
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b1.HasKey("BookId");
@@ -261,16 +266,16 @@ namespace DataLayer.Migrations
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
@@ -279,7 +284,19 @@ namespace DataLayer.Migrations
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Category");
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
@@ -295,6 +312,10 @@ namespace DataLayer.Migrations
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
@@ -302,6 +323,8 @@ namespace DataLayer.Migrations
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
@@ -311,6 +334,8 @@ namespace DataLayer.Migrations
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
@@ -326,6 +351,27 @@ namespace DataLayer.Migrations
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}

View File

@@ -8,14 +8,11 @@ namespace DataLayer
{
public static class BookQueries
{
public static Book GetBook_Flat_NoTracking(string productId)
{
using var context = LibationContext.Create();
return context
public static Book GetBook_Flat_NoTracking(this LibationContext context, string productId)
=> context
.Books
.AsNoTracking()
.GetBook(productId);
}
public static Book GetBook(this IQueryable<Book> books, string productId)
=> books

View File

@@ -5,25 +5,25 @@ using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public static class LibraryQueries
{
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
{
using var context = LibationContext.Create();
return context
{
public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
=> context
.Library
.GetLibrary()
.ToList();
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context)
=> context
.Library
//.AsNoTracking()
.AsNoTracking()
.GetLibrary()
.ToList();
}
public static LibraryBook GetLibraryBook_Flat_NoTracking(string productId)
{
using var context = LibationContext.Create();
return context
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
=> context
.Library
//.AsNoTracking()
.GetLibraryBook(productId);
}
.AsNoTracking()
.GetLibraryBook(productId);
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
public static IQueryable<LibraryBook> GetLibrary(this IQueryable<LibraryBook> library)

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core.Collections.Generic;
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
internal class TagPersistenceInterceptor : IDbInterceptor
{
public void Executed(DbContext context) { }
public void Executing(DbContext context)
{
var tagsCollection
= context
.ChangeTracker
.Entries()
.Where(e => e.State.In(EntityState.Modified, EntityState.Added))
.Select(e => e.Entity as UserDefinedItem)
.Where(udi => udi != null)
// do NOT filter out entires with blank tags. blank is the valid way to show the absence of tags
.Select(t => (t.Book.AudibleProductId, t.Tags))
.ToList();
FileManager.TagsPersistence.Save(tagsCollection);
}
}
}

View File

@@ -1,82 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
namespace DataLayer
{
public class Contributor
{
// contributors search links are just name with url-encoding. space can be + or %20
// author search link: /search?searchAuthor=Robert+Bevan
// narrator search link: /search?searchNarrator=Robert+Bevan
// can also search multiples. concat with comma before url encode
// id.s
// ----
// https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2
// goes to summary page
// at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman
// some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears
// all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman
internal int ContributorId { get; private set; }
public string Name { get; private set; }
private HashSet<BookContributor> _booksLink;
public IEnumerable<BookContributor> BooksLink => _booksLink?.ToList();
private Contributor() { }
public Contributor(string name)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
_booksLink = new HashSet<BookContributor>();
Name = name;
}
public string AudibleAuthorId { get; private set; }
public void UpdateAudibleAuthorId(string authorId)
{
// don't overwrite with null or whitespace but not an error
if (!string.IsNullOrWhiteSpace(authorId))
AudibleAuthorId = authorId;
}
#region // AudibleAuthorId refactor: separate author-specific info. overkill for a single optional string
///// <summary>Most authors in Audible have a unique id</summary>
//public AudibleAuthorProperty AudibleAuthorProperty { get; private set; }
//public void UpdateAuthorId(string authorId, LibationContext context = null)
//{
// if (authorId == null)
// return;
// if (AudibleAuthorProperty != null)
// {
// AudibleAuthorProperty.UpdateAudibleAuthorId(authorId);
// return;
// }
// if (context == null)
// throw new ArgumentNullException(nameof(context), "You must provide a context");
// if (context.Contributors.Find(ContributorId) == null)
// throw new InvalidOperationException("Could not update audible author id.");
// var audibleAuthorProperty = new AudibleAuthorProperty();
// audibleAuthorProperty.UpdateAudibleAuthorId(authorId);
// context.AuthorProperties.Add(audibleAuthorProperty);
//}
//public class AudibleAuthorProperty
//{
// public int ContributorId { get; private set; }
// public Contributor Contributor { get; set; }
// public string AudibleAuthorId { get; private set; }
// public void UpdateAudibleAuthorId(string authorId)
// {
// if (!string.IsNullOrWhiteSpace(authorId))
// AudibleAuthorId = authorId;
// }
//}
//// ...and create EF table config
#endregion
}
}

View File

@@ -1,21 +0,0 @@
using System;
using Dinah.Core;
namespace DataLayer
{
public class LibraryBook
{
internal int BookId { get; private set; }
public Book Book { get; private set; }
public DateTime DateAdded { get; private set; }
private LibraryBook() { }
public LibraryBook(Book book, DateTime dateAdded)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
Book = book;
DateAdded = dateAdded;
}
}
}

View File

@@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core.Collections.Generic;
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
internal class TagPersistenceInterceptor : IDbInterceptor
{
public void Executing(DbContext context)
{
doWork__EFCore(context);
}
public void Executed(DbContext context) { }
static void doWork__EFCore(DbContext context)
{
// persist tags:
var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State.In(EntityState.Modified, EntityState.Added)).ToList();
var tagSets = modifiedEntities.Select(e => e.Entity as UserDefinedItem).Where(a => a != null).ToList();
foreach (var t in tagSets)
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
}
#region // notes: working with proxies, esp EF 6
// EF 6: entities are proxied with lazy loading when collections are virtual
// EF Core: lazy loading is supported in 2.1 (there is a version of lazy loading with proxy-wrapping and a proxy-less version with DI) but not on by default and are not supported here
//static void doWork_EF6(DbContext context)
//{
// var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State == EntityState.Modified).ToList();
// var unproxiedEntities = modifiedEntities.Select(me => UnProxy(context, me.Entity)).ToList();
// // persist tags
// var tagSets = unproxiedEntities.Select(ue => ue as UserDefinedItem).Where(a => a != null).ToList();
// foreach (var t in tagSets)
// FileManager.TagsPersistence.Save(t.ProductId, t.TagsRaw);
//}
//// https://stackoverflow.com/a/25774651
//private static T UnProxy<T>(DbContext context, T proxyObject) where T : class
//{
// // alternative: https://docs.microsoft.com/en-us/ef/ef6/fundamentals/proxies
// var proxyCreationEnabled = context.Configuration.ProxyCreationEnabled;
// try
// {
// context.Configuration.ProxyCreationEnabled = false;
// return context.Entry(proxyObject).CurrentValues.ToObject() as T;
// }
// finally
// {
// context.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
// }
//}
#endregion
}
}

View File

@@ -1,122 +0,0 @@
using System;
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace _scratch_pad
{
////// to use this as a console, open properties and change from class library => console
//// DON'T FORGET TO REVERT IT
//public class Program
//{
// public static void Main(string[] args)
// {
// var user = new Student() { Name = "Dinah Cheshire" };
// var udi = new UserDef { UserDefId = 1, TagsRaw = "my,tags" };
// using var context = new MyTestContextDesignTimeDbContextFactory().Create();
// context.Add(user);
// //context.Add(udi);
// context.Update(udi);
// context.SaveChanges();
// Console.WriteLine($"Student was saved in the database with id: {user.Id}");
// }
//}
public class MyTestContextDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase<MyTestContext>
{
protected override MyTestContext CreateNewInstance(DbContextOptions<MyTestContext> options) => new MyTestContext(options);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString);
}
public class MyTestContext : DbContext
{
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
public MyTestContext(DbContextOptions<MyTestContext> options) : base(options) { }
#region classes for OnModelCreating() seed example
class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public System.Collections.Generic.ICollection<Post> Posts { get; set; }
}
class Post
{
public int PostId { get; set; }
public string Content { get; set; }
public string Title { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
public Name AuthorName { get; set; }
}
class Name
{
public string First { get; set; }
public string Last { get; set; }
}
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// config
modelBuilder.Entity<Blog>(entity => entity.Property(e => e.Url).IsRequired());
modelBuilder.Entity<Order>().OwnsOne(p => p.OrderDetails, cb =>
{
cb.OwnsOne(c => c.BillingAddress);
cb.OwnsOne(c => c.ShippingAddress);
});
modelBuilder.Entity<Post>(entity =>
entity
.HasOne(d => d.Blog)
.WithMany(p => p.Posts)
.HasForeignKey("BlogId"));
// BlogSeed
modelBuilder.Entity<Blog>().HasData(new Blog { BlogId = 1, Url = "http://sample.com" });
// PostSeed
modelBuilder.Entity<Post>().HasData(new Post() { BlogId = 1, PostId = 1, Title = "First post", Content = "Test 1" });
// AnonymousPostSeed
modelBuilder.Entity<Post>().HasData(new { BlogId = 1, PostId = 2, Title = "Second post", Content = "Test 2" });
// OwnedTypeSeed
modelBuilder.Entity<Post>().OwnsOne(p => p.AuthorName).HasData(
new { PostId = 1, First = "Andriy", Last = "Svyryd" },
new { PostId = 2, First = "Diego", Last = "Vega" });
}
public DbSet<Student> Students { get; set; }
public DbSet<UserDef> UserDefs { get; set; }
public DbSet<Order> Orders { get; set; }
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
public class UserDef
{
public int UserDefId { get; set; }
public string TagsRaw { get; set; }
}
public class Order
{
public int Id { get; set; }
public OrderDetails OrderDetails { get; set; }
}
public class OrderDetails
{
public StreetAddress BillingAddress { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
public class StreetAddress
{
public string Street { get; set; }
public string City { get; set; }
}
}

View File

@@ -1,4 +1,8 @@
HOW TO CREATE: EF CORE PROJECT
FOR QUICK MIGRATION INSTRUCTIONS:
_DB_NOTES.txt
HOW TO CREATE: EF CORE PROJECT
==============================
example is for sqlite but the same works with MsSql
@@ -7,15 +11,22 @@ nuget
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
Microsoft.EntityFrameworkCore.Sqlite
MIGRATIONS require standard, not core
using standard instead of core. edit 3 things in csproj
1of3: pluralize xml TargetFramework tag to TargetFrameworks
2of2: TargetFrameworks from: netstandard2.1
to: netcoreapp3.0;netstandard2.1
3of3: add
<PropertyGroup>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
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'.

View File

@@ -1,9 +1,6 @@
{
"ConnectionStrings": {
"LibationContext_sqlserver": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;",
"// on windows sqlite paths accept windows and/or unix slashes": "",
"MyTestContext": "Data Source=%DESKTOP%/sample.db"
"// this connection string is ONLY used for DataLayer's Migrations. this appsettings.json file is NOT used at all by application; it is overwritten": "",
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;"
}
}

View File

@@ -9,29 +9,31 @@ namespace DtoImporterService
{
public class BookImporter : ItemsImporterBase
{
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new BookValidator().Validate(items);
public BookImporter(LibationContext context) : base(context) { }
protected override int DoImport(IEnumerable<Item> items, LibationContext context)
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new BookValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// pre-req.s
new ContributorImporter().Import(items, context);
new SeriesImporter().Import(items, context);
new CategoryImporter().Import(items, context);
new ContributorImporter(DbContext).Import(importItems);
new SeriesImporter(DbContext).Import(importItems);
new CategoryImporter(DbContext).Import(importItems);
// get distinct
var productIds = items.Select(i => i.ProductId).ToList();
var productIds = importItems.Select(i => i.DtoItem.ProductId).ToList();
// load db existing => .Local
loadLocal_books(productIds, context);
loadLocal_books(productIds);
// upsert
var qtyNew = upsertBooks(items, context);
var qtyNew = upsertBooks(importItems);
return qtyNew;
}
private void loadLocal_books(List<string> productIds, LibationContext context)
private void loadLocal_books(List<string> productIds)
{
var localProductIds = context.Books.Local.Select(b => b.AudibleProductId);
var localProductIds = DbContext.Books.Local.Select(b => b.AudibleProductId);
var remainingProductIds = productIds
.Distinct()
.Except(localProductIds)
@@ -39,87 +41,129 @@ namespace DtoImporterService
// GetBooks() eager loads Series, category, et al
if (remainingProductIds.Any())
context.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
DbContext.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
}
private int upsertBooks(IEnumerable<Item> items, LibationContext context)
private int upsertBooks(IEnumerable<ImportItem> importItems)
{
var qtyNew = 0;
foreach (var item in items)
foreach (var item in importItems)
{
var book = context.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.ProductId);
var book = DbContext.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
if (book is null)
{
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var authors = item
.Authors
.Select(a => context.Contributors.Local.Single(c => a.Name == c.Name))
.ToList();
book = context.Books.Add(new Book(
new AudibleProductId(item.ProductId), item.Title, item.Description, item.LengthInMinutes, authors))
.Entity;
book = createNewBook(item);
qtyNew++;
}
// if no narrators listed, author is the narrator
if (item.Narrators is null || !item.Narrators.Any())
item.Narrators = item.Authors;
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var narrators = item
.Narrators
.Select(n => context.Contributors.Local.Single(c => n.Name == c.Name))
.ToList();
// not all books have narrators. these will already be using author as narrator. don't undo this
if (narrators.Any())
book.ReplaceNarrators(narrators);
// set/update book-specific info which may have changed
book.PictureId = item.PictureId;
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
book.AddSupplementDownloadUrl(item.SupplementUrl);
var publisherName = item.Publisher;
if (!string.IsNullOrWhiteSpace(publisherName))
{
var publisher = context.Contributors.Local.Single(c => publisherName == c.Name);
book.ReplacePublisher(publisher);
}
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
//
// this was round 1 when it was a 2 step process
//
//// update series even for existing books. these are occasionally updated
//var seriesIds = item.Series.Select(kvp => kvp.SeriesId).ToList();
//var allSeries = context.Series.Local.Where(c => seriesIds.Contains(c.AudibleSeriesId)).ToList();
//foreach (var series in allSeries)
// book.UpsertSeries(series);
// these will upsert over library-scraped series, but will not leave orphans
if (item.Series != null)
{
foreach (var seriesEntry in item.Series)
{
var series = context.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId);
book.UpsertSeries(series, seriesEntry.Index);
}
}
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
var category = context.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == item.Categories.LastOrDefault().CategoryId);
if (category != null)
book.UpdateCategory(category, context);
book.UpdateBookDetails(item.IsAbridged, item.DatePublished);
updateBook(item, book);
}
return qtyNew;
}
private Book createNewBook(ImportItem importItem)
{
var item = importItem.DtoItem;
//Add any subtitle after the title title.
var title = item.Title + (!string.IsNullOrWhiteSpace(item.Subtitle) ? $": {item.Subtitle}" : "");
// absence of authors is very rare, but possible
if (!item.Authors?.Any() ?? true)
item.Authors = new[] { new Person { Name = "", Asin = null } };
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var authors = item
.Authors
.Select(a => DbContext.Contributors.Local.Single(c => a.Name == c.Name))
.ToList();
var narrators
= item.Narrators is null || !item.Narrators.Any()
// if no narrators listed, author is the narrator
? authors
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
: item
.Narrators
.Select(n => DbContext.Contributors.Local.Single(c => n.Name == c.Name))
.ToList();
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
// absence of categories is also possible
// CATEGORY HACK: only use the 1st 2 categories
// (real impl: var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";)
var lastCategory
= item.Categories.Length == 0 ? ""
: item.Categories.Length == 1 ? item.Categories[0].CategoryId
// 2+
: item.Categories[1].CategoryId;
var category = DbContext.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == lastCategory);
var book = DbContext.Books.Add(new Book(
new AudibleProductId(item.ProductId),
title,
item.Description,
item.LengthInMinutes,
authors,
narrators,
category,
importItem.LocaleName)
).Entity;
var publisherName = item.Publisher;
if (!string.IsNullOrWhiteSpace(publisherName))
{
var publisher = DbContext.Contributors.Local.Single(c => publisherName == c.Name);
book.ReplacePublisher(publisher);
}
book.UpdateBookDetails(item.IsAbridged, item.DatePublished);
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
book.AddSupplementDownloadUrl(item.SupplementUrl);
return book;
}
private void updateBook(ImportItem importItem, Book book)
{
var item = importItem.DtoItem;
// set/update book-specific info which may have changed
book.PictureId = item.PictureId;
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
// needed during v3 => v4 migration
book.UpdateLocale(importItem.LocaleName);
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
// update series even for existing books. these are occasionally updated
// these will upsert over library-scraped series, but will not leave orphans
if (item.Series != null)
{
foreach (var seriesEntry in item.Series)
{
var series = DbContext.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId);
var index = 0f;
try
{
index = seriesEntry.Index;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Error parsing series index. Title: {item.Title}. ASIN: {item.Asin}. Series index: {seriesEntry.Sequence}");
}
book.UpsertSeries(series, index);
}
}
}
}
}

View File

@@ -9,36 +9,47 @@ namespace DtoImporterService
{
public class CategoryImporter : ItemsImporterBase
{
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new CategoryValidator().Validate(items);
public CategoryImporter(LibationContext context) : base(context) { }
protected override int DoImport(IEnumerable<Item> items, LibationContext context)
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new CategoryValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// get distinct
var categoryIds = items.GetCategoriesDistinct().Select(c => c.CategoryId).ToList();
var categoryIds = importItems
.Select(i => i.DtoItem)
.GetCategoriesDistinct()
.Select(c => c.CategoryId).ToList();
// load db existing => .Local
loadLocal_categories(categoryIds, context);
loadLocal_categories(categoryIds);
// upsert
var categoryPairs = items.GetCategoryPairsDistinct().ToList();
var qtyNew = upsertCategories(categoryPairs, context);
var categoryPairs = importItems
.Select(i => i.DtoItem)
.GetCategoryPairsDistinct()
.ToList();
var qtyNew = upsertCategories(categoryPairs);
return qtyNew;
}
private void loadLocal_categories(List<string> categoryIds, LibationContext context)
private void loadLocal_categories(List<string> categoryIds)
{
var localIds = context.Categories.Local.Select(c => c.AudibleCategoryId);
var localIds = DbContext.Categories.Local.Select(c => c.AudibleCategoryId);
var remainingCategoryIds = categoryIds
.Distinct()
.Except(localIds)
.ToList();
// load existing => local
// remember to include default/empty/missing
var emptyName = Contributor.GetEmpty().Name;
if (remainingCategoryIds.Any())
context.Categories.Where(c => remainingCategoryIds.Contains(c.AudibleCategoryId)).ToList();
DbContext.Categories.Where(c => remainingCategoryIds.Contains(c.AudibleCategoryId) || c.Name == emptyName).ToList();
}
// only use after loading contributors => local
private int upsertCategories(List<Ladder[]> categoryPairs, LibationContext context)
private int upsertCategories(List<Ladder[]> categoryPairs)
{
var qtyNew = 0;
@@ -46,17 +57,21 @@ namespace DtoImporterService
{
for (var i = 0; i < pair.Length; i++)
{
// CATEGORY HACK: not yet supported: depth beyond 0 and 1
if (i > 1)
break;
var id = pair[i].CategoryId;
var name = pair[i].CategoryName;
Category parentCategory = null;
if (i == 1)
parentCategory = context.Categories.Local.Single(c => c.AudibleCategoryId == pair[0].CategoryId);
parentCategory = DbContext.Categories.Local.Single(c => c.AudibleCategoryId == pair[0].CategoryId);
var category = context.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == id);
var category = DbContext.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == id);
if (category is null)
{
category = context.Categories.Add(new Category(new AudibleCategoryId(id), name)).Entity;
category = DbContext.Categories.Add(new Category(new AudibleCategoryId(id), name)).Entity;
qtyNew++;
}

View File

@@ -9,84 +9,90 @@ namespace DtoImporterService
{
public class ContributorImporter : ItemsImporterBase
{
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new ContributorValidator().Validate(items);
public ContributorImporter(LibationContext context) : base(context) { }
protected override int DoImport(IEnumerable<Item> items, LibationContext 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 = items.GetAuthorsDistinct().ToList();
var narrators = items.GetNarratorsDistinct().ToList();
var publishers = items.GetPublishersDistinct().ToList();
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 = authors
.Select(a => a.Name)
var allNames = publishers
.Union(authors.Select(n => n.Name))
.Union(narrators.Select(n => n.Name))
.Union(publishers)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
loadLocal_contributors(allNames, context);
loadLocal_contributors(allNames);
// upsert
var qtyNew = 0;
qtyNew += upsertPeople(authors, context);
qtyNew += upsertPeople(narrators, context);
qtyNew += upsertPublishers(publishers, context);
qtyNew += upsertPeople(authors);
qtyNew += upsertPeople(narrators);
qtyNew += upsertPublishers(publishers);
return qtyNew;
}
private void loadLocal_contributors(List<string> contributorNames, LibationContext context)
private void loadLocal_contributors(List<string> contributorNames)
{
contributorNames.Remove(null);
contributorNames.Remove("");
//// 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 = context.Contributors.Local.Select(c => c.Name);
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())
context.Contributors.Where(c => remainingContribNames.Contains(c.Name)).ToList();
// _________________________________^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// i tried to extract this pattern, but this part prohibits doing so
// wouldn't work anyway for Books.GetBooks()
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name) || c.Name == emptyName).ToList();
}
// only use after loading contributors => local
private int upsertPeople(List<Person> people, LibationContext context)
private int upsertPeople(List<Person> people)
{
var qtyNew = 0;
foreach (var p in people)
{
var person = context.Contributors.Local.SingleOrDefault(c => c.Name == p.Name);
// 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 = context.Contributors.Add(new Contributor(p.Name)).Entity;
person = DbContext.Contributors.Add(new Contributor(p.Name, p.Asin)).Entity;
qtyNew++;
}
person.UpdateAudibleAuthorId(p.Asin);
}
return qtyNew;
}
// only use after loading contributors => local
private int upsertPublishers(List<string> publishers, LibationContext context)
private int upsertPublishers(List<string> publishers)
{
var qtyNew = 0;
foreach (var publisherName in publishers)
{
if (context.Contributors.Local.SingleOrDefault(c => c.Name == publisherName) == null)
if (DbContext.Contributors.Local.SingleOrDefault(c => c.Name == publisherName) == null)
{
context.Contributors.Add(new Contributor(publisherName));
DbContext.Contributors.Add(new Contributor(publisherName));
qtyNew++;
}
}

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