Compare commits
700 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e368e4669b | ||
|
|
c6ce814e1c | ||
|
|
dd5e162c10 | ||
|
|
7af890d897 | ||
|
|
0faeeea25f | ||
|
|
de9b3fd6ec | ||
|
|
22e5c8746c | ||
|
|
0091245734 | ||
|
|
448c231cfa | ||
|
|
b0d1f692a3 | ||
|
|
a5ff890ea1 | ||
|
|
df4739cbf4 | ||
|
|
9559109aa8 | ||
|
|
d848c1a499 | ||
|
|
48ffc40abb | ||
|
|
82b5daa809 | ||
|
|
b320276926 | ||
|
|
6ccb8d612f | ||
|
|
23460e0137 | ||
|
|
7723de7284 | ||
|
|
138f94594f | ||
|
|
81c152ddcb | ||
|
|
04665fea36 | ||
|
|
803eef3825 | ||
|
|
e2a05761a6 | ||
|
|
b1968caa0f | ||
|
|
6474ef98f5 | ||
|
|
8763d63a93 | ||
|
|
201ecebda9 | ||
|
|
1c9ea0a710 | ||
|
|
30feb42ed8 | ||
|
|
cfe2eac351 | ||
|
|
725979afb0 | ||
|
|
19262bceac | ||
|
|
6254585ae2 | ||
|
|
5ad1e45c65 | ||
|
|
9fe95bbddc | ||
|
|
aecc54401d | ||
|
|
4332732cc4 | ||
|
|
15b6b673d7 | ||
|
|
cb8b5d74d7 | ||
|
|
535dc7a038 | ||
|
|
c6f7b142ee | ||
|
|
4853b26cd3 | ||
|
|
c99ee56f24 | ||
|
|
f6a22db188 | ||
|
|
c26b31f548 | ||
|
|
96654c599b | ||
|
|
57be386ac7 | ||
|
|
09f782bfbd | ||
|
|
cb673e34c1 | ||
|
|
62af7ab70c | ||
|
|
71f2f656e8 | ||
|
|
fddf096de6 | ||
|
|
9b8cf69148 | ||
|
|
628c80048b | ||
|
|
9daa264af4 | ||
|
|
8f8a8e7340 | ||
|
|
389fbb2371 | ||
|
|
1ee73fa1a7 | ||
|
|
adbbff368f | ||
|
|
ee9d30bd56 | ||
|
|
5a822809a9 | ||
|
|
4e587e0429 | ||
|
|
9a619186fd | ||
|
|
eab6f71a4c | ||
|
|
f68bf2d6b3 | ||
|
|
2afcaebb78 | ||
|
|
458ea6a377 | ||
|
|
0e2997d309 | ||
|
|
420f4b9d5d | ||
|
|
bcfa97219f | ||
|
|
4c66010afe | ||
|
|
05f25a88c6 | ||
|
|
2c6c08fbb5 | ||
|
|
8af60b56b6 | ||
|
|
c0516772a7 | ||
|
|
77de70762c | ||
|
|
7164100cb1 | ||
|
|
9292a62015 | ||
|
|
5280e68da9 | ||
|
|
0f6b0bf9fe | ||
|
|
510ed95590 | ||
|
|
9862593f4a | ||
|
|
d595b62f13 | ||
|
|
12abbb79b1 | ||
|
|
ecaa3b9aab | ||
|
|
ded175f2d2 | ||
|
|
128facec21 | ||
|
|
0bde86ebfd | ||
|
|
28625029cd | ||
|
|
1816bd721c | ||
|
|
68ad627159 | ||
|
|
878a5dd36c | ||
|
|
7c144b8277 | ||
|
|
bca8c3865b | ||
|
|
58102acd35 | ||
|
|
5e577843f7 | ||
|
|
e1d549cead | ||
|
|
323b8f2fb9 | ||
|
|
3dcbcf42ed | ||
|
|
825078abc6 | ||
|
|
6be44966ad | ||
|
|
66da138556 | ||
|
|
e5dd4b856e | ||
|
|
5caa9c5687 | ||
|
|
c8c0ffeb0d | ||
|
|
bfceb58d6b | ||
|
|
2e4c4cf5f7 | ||
|
|
23966c9b00 | ||
|
|
ef73d2243d | ||
|
|
c95feebd39 | ||
|
|
d6601fed83 | ||
|
|
962e379642 | ||
|
|
cbc61f5a2d | ||
|
|
2eaac6acc2 | ||
|
|
03b458765c | ||
|
|
c8b4bc6361 | ||
|
|
d9b5725ff1 | ||
|
|
0a0f60192b | ||
|
|
424d939c15 | ||
|
|
87f13ff8ed | ||
|
|
1e24df626a | ||
|
|
0312786721 | ||
|
|
1f8a5b256e | ||
|
|
426391f01c | ||
|
|
c296bff47f | ||
|
|
6b649cf4ca | ||
|
|
5103240a76 | ||
|
|
c2418b10f6 | ||
|
|
d705c23472 | ||
|
|
de45d008c7 | ||
|
|
c267332027 | ||
|
|
4829e85faf | ||
|
|
2acb9ca7e5 | ||
|
|
b260554a2a | ||
|
|
41a4055cd9 | ||
|
|
c6e9ba9bf9 | ||
|
|
5059333b38 | ||
|
|
b4015030cf | ||
|
|
7f5cf8f018 | ||
|
|
2c9ccd9c78 | ||
|
|
cebf218db4 | ||
|
|
530b44a0e6 | ||
|
|
b3dc5a7054 | ||
|
|
2567ccb44c | ||
|
|
e67eac92fd | ||
|
|
6e84fd97f1 | ||
|
|
9a458bf3dc | ||
|
|
283a46e1e2 | ||
|
|
6ff2859c39 | ||
|
|
e8df4952fc | ||
|
|
b19e1e8a30 | ||
|
|
a3cf6ac40d | ||
|
|
ab450c37c4 | ||
|
|
c837fefbdd | ||
|
|
46b120ee41 | ||
|
|
cae8ca7ef3 | ||
|
|
904665da7f | ||
|
|
2478c61df6 | ||
|
|
288ed75b5d | ||
|
|
ad5efbd9a9 | ||
|
|
7eb7b2a0f9 | ||
|
|
d0051c0f02 | ||
|
|
d20517063e | ||
|
|
bcca69a102 | ||
|
|
35f8c05106 | ||
|
|
a3d38e082d | ||
|
|
b2e956e70b | ||
|
|
e5119357b2 | ||
|
|
b42ff827d5 | ||
|
|
68da9779da | ||
|
|
8e358d8f04 | ||
|
|
0a986238bc | ||
|
|
d636ceed8e | ||
|
|
e4fc104afe | ||
|
|
87e3075fb3 | ||
|
|
ab44823c05 | ||
|
|
2767f04621 | ||
|
|
0f1ff0aa10 | ||
|
|
c1af253300 | ||
|
|
d08962cffa | ||
|
|
7720110460 | ||
|
|
dfa5829cbd | ||
|
|
648b84ee55 | ||
|
|
6a81b9b02d | ||
|
|
c43e03b228 | ||
|
|
1de7edd9df | ||
|
|
df90094884 | ||
|
|
c9a6c8fd35 | ||
|
|
d0b78cc501 | ||
|
|
0b7bc4d938 | ||
|
|
18cca53968 | ||
|
|
ef9c60cc4f | ||
|
|
fa24831693 | ||
|
|
24370e9804 | ||
|
|
d3f82b162e | ||
|
|
5a40c7370f | ||
|
|
2d22855b93 | ||
|
|
b870d562ff | ||
|
|
f1c87308ea | ||
|
|
a3fac3441c | ||
|
|
5f8c672361 | ||
|
|
40520b89d1 | ||
|
|
0ac90f5a30 | ||
|
|
4d6544d828 | ||
|
|
8098564926 | ||
|
|
07c96c4994 | ||
|
|
aa8491f205 | ||
|
|
5c535478d1 | ||
|
|
f0541b498f | ||
|
|
e466d63e76 | ||
|
|
6e66314605 | ||
|
|
be5e18d977 | ||
|
|
c437a39a82 | ||
|
|
7b55158148 | ||
|
|
5772d9c31e | ||
|
|
2a1f02b095 | ||
|
|
5b7cde2a9e | ||
|
|
5e349c6662 | ||
|
|
4b78b757aa | ||
|
|
22548dc8ae | ||
|
|
1165f81203 | ||
|
|
13294d3414 | ||
|
|
8a74a29700 | ||
|
|
36f58b64d6 | ||
|
|
19369a21ef | ||
|
|
611fb4d6d8 | ||
|
|
c77ec54035 | ||
|
|
c9c28c7826 | ||
|
|
30e2caaff5 | ||
|
|
fd56017af5 | ||
|
|
d2eaf26117 | ||
|
|
7c38e18435 | ||
|
|
bfb1dbc69a | ||
|
|
d2ff19e309 | ||
|
|
aa3a7dce06 | ||
|
|
71075838eb | ||
|
|
803a0b7ccf | ||
|
|
d9f3fa825c | ||
|
|
df42ba584e | ||
|
|
9f09a62a1e | ||
|
|
e714179c30 | ||
|
|
db84c9a7d9 | ||
|
|
937bd56fcc | ||
|
|
f29968f379 | ||
|
|
14e14ba9bd | ||
|
|
613c97524a | ||
|
|
4fd16f04e0 | ||
|
|
61385f0f0b | ||
|
|
7647882344 | ||
|
|
96ffa619ec | ||
|
|
de1147ac1b | ||
|
|
926a7a1148 | ||
|
|
51020ef99e | ||
|
|
5a1303c33a | ||
|
|
a0e2d78b9b | ||
|
|
6b711190c3 | ||
|
|
b4a6342513 | ||
|
|
988b137d67 | ||
|
|
dae9c9c9b6 | ||
|
|
420b7529c6 | ||
|
|
4cf999c84d | ||
|
|
8fe3896d76 | ||
|
|
adcba34560 | ||
|
|
8e09d7e617 | ||
|
|
197b50e3ac | ||
|
|
ac2114e270 | ||
|
|
29461701cd | ||
|
|
0f130c70f5 | ||
|
|
995637e843 | ||
|
|
9501687f86 | ||
|
|
248dea3402 | ||
|
|
1d420f5430 | ||
|
|
5f0a6b8526 | ||
|
|
c337c0b44e | ||
|
|
89207866f3 | ||
|
|
9e11086d49 | ||
|
|
58b172f816 | ||
|
|
0b8084bc03 | ||
|
|
37970222f3 | ||
|
|
bcab2dd440 | ||
|
|
d402128d1d | ||
|
|
3ae0f2daa2 | ||
|
|
126919d578 | ||
|
|
437e85fd12 | ||
|
|
de34e5c795 | ||
|
|
8ffcefd6ae | ||
|
|
e59ab9b483 | ||
|
|
57fa1bd763 | ||
|
|
dccb2d73d6 | ||
|
|
77fc865636 | ||
|
|
1040a347c6 | ||
|
|
6ed1307443 | ||
|
|
c2c732b2b1 | ||
|
|
9e0caf34d6 | ||
|
|
802763a4fb | ||
|
|
b4803c42a5 | ||
|
|
62c98c66a3 | ||
|
|
6b289445e2 | ||
|
|
52bf91f8aa | ||
|
|
6d2dff1a98 | ||
|
|
7c9970c0cb | ||
|
|
d2892f9076 | ||
|
|
89f60a7ca3 | ||
|
|
ea37c09081 | ||
|
|
76cb280933 | ||
|
|
0a54a8104c | ||
|
|
7464336535 | ||
|
|
dc0dd3474b | ||
|
|
7b9c5c0f4f | ||
|
|
ad87f1851e | ||
|
|
e8423341ef | ||
|
|
a9d3494af1 | ||
|
|
90731a8948 | ||
|
|
e723467ca6 | ||
|
|
722c33bf61 | ||
|
|
f080215cbb | ||
|
|
d5c74d629f | ||
|
|
d12c246f6d | ||
|
|
8969c216af | ||
|
|
9a4903f0dd | ||
|
|
3eda498a5e | ||
|
|
8af7f28f04 | ||
|
|
d9d7dfe1f7 | ||
|
|
b9c4d11946 | ||
|
|
68a5d7a58d | ||
|
|
4d69b222c5 | ||
|
|
42f94e7f6c | ||
|
|
381d52be72 | ||
|
|
f16ad30891 | ||
|
|
ef53a6a8cb | ||
|
|
9a37d434f1 | ||
|
|
d7eb190f69 | ||
|
|
f19c46ee45 | ||
|
|
343c3b62d6 | ||
|
|
b1de10a71a | ||
|
|
6beb5cc74a | ||
|
|
3767c3574a | ||
|
|
4ceb4f9c03 | ||
|
|
0f5149f7b4 | ||
|
|
673451dc11 | ||
|
|
e4257afc14 | ||
|
|
2a7e185dc3 | ||
|
|
9e06c343c1 | ||
|
|
40b3a9990d | ||
|
|
d66c112a1e | ||
|
|
d826885728 | ||
|
|
263222d8cc | ||
|
|
f25734334d | ||
|
|
ede8397f13 | ||
|
|
1369ee575a | ||
|
|
c8e2418af7 | ||
|
|
2da25edafd | ||
|
|
f60964f4c7 | ||
|
|
3183f99153 | ||
|
|
2a22cff67c | ||
|
|
7fbe8ae769 | ||
|
|
f9df466ad8 | ||
|
|
0b129fcf7c | ||
|
|
2be5fd5af3 | ||
|
|
c9727f84ab | ||
|
|
aa56bb74a1 | ||
|
|
85a6e21dcf | ||
|
|
8c620c25ab | ||
|
|
813d91dfa4 | ||
|
|
d0d66c6135 | ||
|
|
a8d609676e | ||
|
|
8386da5ec6 | ||
|
|
f5089e7e29 | ||
|
|
a639857ec6 | ||
|
|
35b5d7370c | ||
|
|
c9f988acf8 | ||
|
|
6dfef09ea3 | ||
|
|
7e288c0c08 | ||
|
|
dbcf6f25db | ||
|
|
88133652e9 | ||
|
|
e768466943 | ||
|
|
0cc55fd1e8 | ||
|
|
e36ea70cd1 | ||
|
|
a86185e644 | ||
|
|
64a8f007a5 | ||
|
|
215a626c92 | ||
|
|
de93047192 | ||
|
|
79c9a094b5 | ||
|
|
012a92ea30 | ||
|
|
2e60d2accf | ||
|
|
565d34cec9 | ||
|
|
dd6967e88b | ||
|
|
fb7f57ab69 | ||
|
|
88253cdb55 | ||
|
|
560880b53d | ||
|
|
27ae5facbe | ||
|
|
7a90d9fba9 | ||
|
|
f74b0d78db | ||
|
|
52fb0a27ce | ||
|
|
7bdcf4eef0 | ||
|
|
a44c46333f | ||
|
|
766d427b19 | ||
|
|
0e7930f2b6 | ||
|
|
081878b6f7 | ||
|
|
f925d10d2b | ||
|
|
e37a2ccca9 | ||
|
|
3e2d69606b | ||
|
|
2c20d03506 | ||
|
|
97730d1793 | ||
|
|
5ab4183f9b | ||
|
|
7acaac7bd3 | ||
|
|
448fd78b8f | ||
|
|
56a48c04bf | ||
|
|
65027fd001 | ||
|
|
f57a46c772 | ||
|
|
a45ab61929 | ||
|
|
cd67e7136b | ||
|
|
265ad3a782 | ||
|
|
9f49a88000 | ||
|
|
b5d941d479 | ||
|
|
79ed92f303 | ||
|
|
1c239dc546 | ||
|
|
687591e08e | ||
|
|
0045cf05ef | ||
|
|
963d632208 | ||
|
|
9c1f620223 | ||
|
|
de75543b33 | ||
|
|
689ffc71a2 | ||
|
|
d795244247 | ||
|
|
4989cda93c | ||
|
|
2f3c0e8a95 | ||
|
|
560523b99d | ||
|
|
d5e9e49517 | ||
|
|
54d24a7b09 | ||
|
|
19a710e080 | ||
|
|
7bdf71a29b | ||
|
|
ef35c2aee9 | ||
|
|
95766a43c5 | ||
|
|
e1dfefbadf | ||
|
|
f81552565a | ||
|
|
957bec1c7f | ||
|
|
5c8ad72a5e | ||
|
|
0b1d513f50 | ||
|
|
d770109d86 | ||
|
|
235d0acede | ||
|
|
6f184273b8 | ||
|
|
a4cb934611 | ||
|
|
6aefdfca9d | ||
|
|
c7454ea5d2 | ||
|
|
2ef746a94c | ||
|
|
ab82e7c99c | ||
|
|
5f8ca9a0b5 | ||
|
|
d48bd5ad07 | ||
|
|
af48641281 | ||
|
|
f621ca63e8 | ||
|
|
35f54779f0 | ||
|
|
f68f374b78 | ||
|
|
7e89386173 | ||
|
|
7685613e8c | ||
|
|
727d1479bb | ||
|
|
bb46021f20 | ||
|
|
c45e6d526c | ||
|
|
a72c3f069b | ||
|
|
1fcacb9cfb | ||
|
|
a3542c53e2 | ||
|
|
9e44a95ba2 | ||
|
|
204e77008b | ||
|
|
621fb68cd8 | ||
|
|
0c265a9010 | ||
|
|
d4fbb03577 | ||
|
|
69a7ab5b0c | ||
|
|
53a46b5dfc | ||
|
|
fb3126b0c6 | ||
|
|
5c6b5c0af2 | ||
|
|
8de8e50829 | ||
|
|
5d15d6c2c7 | ||
|
|
85c18c8334 | ||
|
|
9de85b649b | ||
|
|
3c1db55a95 | ||
|
|
4e6011711a | ||
|
|
1440b3fcf6 | ||
|
|
f2f0725c68 | ||
|
|
75f1d987fc | ||
|
|
de8589fb84 | ||
|
|
54ceba816a | ||
|
|
05d52e64e5 | ||
|
|
5c6bf300c6 | ||
|
|
10ff95161b | ||
|
|
112671cf9f | ||
|
|
1a37b2346e | ||
|
|
54cceba4e3 | ||
|
|
1502936cd0 | ||
|
|
f06b04ede4 | ||
|
|
406aea6ead | ||
|
|
5f8c40962a | ||
|
|
a77405c632 | ||
|
|
fdff31b69f | ||
|
|
f5e1667368 | ||
|
|
af81367b46 | ||
|
|
cd418e877d | ||
|
|
b6c9a82c68 | ||
|
|
efca1f9c1d | ||
|
|
ca14db79b9 | ||
|
|
9d00da006c | ||
|
|
b479096fc2 | ||
|
|
ad09d36588 | ||
|
|
1a9c0188a4 | ||
|
|
ca75b55da4 | ||
|
|
285b1e7b45 | ||
|
|
6912a499d0 | ||
|
|
4e70365150 | ||
|
|
811a95aedf | ||
|
|
20971124ab | ||
|
|
fa66a361dc | ||
|
|
61d7f5a5cb | ||
|
|
f8c788297e | ||
|
|
79e5545fd3 | ||
|
|
b4def2e2d6 | ||
|
|
281d615649 | ||
|
|
c2c6a31716 | ||
|
|
391f1f387b | ||
|
|
206890b8f3 | ||
|
|
9aa31338d6 | ||
|
|
35fe3ae786 | ||
|
|
b6fe3ae009 | ||
|
|
6ba8c0ca91 | ||
|
|
01de928b7a | ||
|
|
0a54f8d881 | ||
|
|
d31121e307 | ||
|
|
b86bd76726 | ||
|
|
c49edbc77b | ||
|
|
1ba54a74af | ||
|
|
9de08a332f | ||
|
|
c9b434daed | ||
|
|
730484c28c | ||
|
|
1a48dbe560 | ||
|
|
7df8c7427c | ||
|
|
8997f52505 | ||
|
|
e61418c677 | ||
|
|
491aa67a81 | ||
|
|
7b3c857042 | ||
|
|
71617b9620 | ||
|
|
f94f66da94 | ||
|
|
2243e2a124 | ||
|
|
5d6e3ea3f3 | ||
|
|
3c1c718bc7 | ||
|
|
20a9e4b651 | ||
|
|
f0daa12bb7 | ||
|
|
c6e1278e42 | ||
|
|
f5e8e4cd7f | ||
|
|
f986462809 | ||
|
|
49f2112c42 | ||
|
|
0ce4faaf29 | ||
|
|
bfd494cf93 | ||
|
|
dc7ec3b328 | ||
|
|
8f2827108b | ||
|
|
fdcaf5e534 | ||
|
|
732695c019 | ||
|
|
2a2faf6f7b | ||
|
|
c653e17c3d | ||
|
|
833bc3a8f2 | ||
|
|
11e63ae5a2 | ||
|
|
827eaefd29 | ||
|
|
8240a97f6d | ||
|
|
b766e43656 | ||
|
|
7d805728cb | ||
|
|
c3c8a6fa6b | ||
|
|
f40df002a2 | ||
|
|
3180ea993c | ||
|
|
9f2fd54018 | ||
|
|
07532f7e65 | ||
|
|
4bae07d36c | ||
|
|
bf23503d67 | ||
|
|
aeeba0d567 | ||
|
|
e2f919d625 | ||
|
|
e821eea333 | ||
|
|
8f487894f5 | ||
|
|
cd3e0dba68 | ||
|
|
6f31d97763 | ||
|
|
fa5637a340 | ||
|
|
7ab209171b | ||
|
|
6d856f73e7 | ||
|
|
05426eb618 | ||
|
|
d73701c939 | ||
|
|
f284f53edd | ||
|
|
17f3187748 | ||
|
|
f55a41ac0a | ||
|
|
0be2a17537 | ||
|
|
b417c5695e | ||
|
|
6efe064ca7 | ||
|
|
da7af895fb | ||
|
|
1b39f30fd0 | ||
|
|
9cde6bddbd | ||
|
|
b21f257baa | ||
|
|
da68ddc9b8 | ||
|
|
9e15fde2e3 | ||
|
|
ef5b14a929 | ||
|
|
5df7d80aac | ||
|
|
4b2c8ee513 | ||
|
|
097bda2d25 | ||
|
|
81195e382e | ||
|
|
35fc3581b3 | ||
|
|
771d992da7 | ||
|
|
00f7e4b779 | ||
|
|
5d4bcb2db0 | ||
|
|
fbf92bf151 | ||
|
|
2cb3e34d98 | ||
|
|
80589e3854 | ||
|
|
b9770220db | ||
|
|
11128ffb1a | ||
|
|
1d557d05c5 | ||
|
|
d41fe0d3e6 | ||
|
|
17bd54a897 | ||
|
|
0d89c34107 | ||
|
|
66bd18fdc5 | ||
|
|
7143104b40 | ||
|
|
729212a5d5 | ||
|
|
6dafa03554 | ||
|
|
08644fb937 | ||
|
|
7ff4953f7b | ||
|
|
797112740e | ||
|
|
36ab494b31 | ||
|
|
8c6ada8d20 | ||
|
|
41b0ace238 | ||
|
|
c84f144274 | ||
|
|
00f8a63781 | ||
|
|
25d89207bb | ||
|
|
2146ebff29 | ||
|
|
3aed3a5def | ||
|
|
1ee6f3b9f2 | ||
|
|
0c26c34bdd | ||
|
|
6696317ae6 | ||
|
|
3af84af2e2 | ||
|
|
2955e8b464 | ||
|
|
8d6b304a8b | ||
|
|
aa3c648c4c | ||
|
|
0da054ccea | ||
|
|
45080d1661 | ||
|
|
d6b62c0521 | ||
|
|
bc3aa29175 | ||
|
|
e958944466 | ||
|
|
78f278121b | ||
|
|
027cce2d99 | ||
|
|
9332a6f350 | ||
|
|
ac6a73d898 | ||
|
|
74f94fe17f | ||
|
|
120fb58da7 | ||
|
|
ef2adfd474 | ||
|
|
f3a746a852 | ||
|
|
dc8cea5355 | ||
|
|
83cb580db7 | ||
|
|
491a5eba3a | ||
|
|
15150a3633 | ||
|
|
82e3854c84 | ||
|
|
f0eb57a40b | ||
|
|
b65f9567e0 | ||
|
|
b5389c67ea | ||
|
|
d564876eaa | ||
|
|
258887152d | ||
|
|
87c3cac013 | ||
|
|
f148650e57 | ||
|
|
b53aabe0e3 | ||
|
|
e32a39085f | ||
|
|
4d743df643 | ||
|
|
6bd809c7c6 | ||
|
|
9930daa914 | ||
|
|
0475bd48b1 | ||
|
|
1b80f2ed28 | ||
|
|
37ca9abd9d | ||
|
|
0c159df6ca | ||
|
|
31e24ad36c | ||
|
|
be41dca9e0 | ||
|
|
792207caee | ||
|
|
06549e5b4e | ||
|
|
81d0f87b8a | ||
|
|
9550aac788 | ||
|
|
54d650ea48 | ||
|
|
1e88070f3a | ||
|
|
703e71ad74 | ||
|
|
ae6384486c | ||
|
|
8f8e0645a4 | ||
|
|
d619c82fd8 | ||
|
|
919175cc10 | ||
|
|
8d70d2a95f | ||
|
|
e13dc2a48a | ||
|
|
bb3baa6ce0 | ||
|
|
28731e51f5 | ||
|
|
dbcd124c1d | ||
|
|
305de34a76 | ||
|
|
0034d51921 | ||
|
|
b1a033e162 | ||
|
|
9416f4e040 | ||
|
|
344e675634 | ||
|
|
372e85d9af | ||
|
|
c81788429b | ||
|
|
26da307743 | ||
|
|
0306c958d1 | ||
|
|
5ec6994da7 | ||
|
|
c1f50a184a | ||
|
|
8231766d2c | ||
|
|
eedc9bb34d | ||
|
|
310b90962c | ||
|
|
54c21e969e |
71
.gitignore
vendored
@@ -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,10 +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
|
||||
|
||||
# manually ignored files
|
||||
/__TODO.txt
|
||||
/DataLayer/LibationContext.db
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Dinah.Core.Diagnostics;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AAXChapters : Chapters
|
||||
{
|
||||
public AAXChapters(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 chaptersXml = xmlDocument.SelectNodes("/ffprobe/chapters/chapter")
|
||||
.Cast<System.Xml.XmlNode>()
|
||||
.Where(n => n.Name == "chapter");
|
||||
|
||||
foreach (var cnode in chaptersXml)
|
||||
{
|
||||
double startTime = double.Parse(cnode.Attributes["start_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture);
|
||||
double endTime = double.Parse(cnode.Attributes["end_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture);
|
||||
|
||||
string chapterTitle = cnode.ChildNodes
|
||||
.Cast<System.Xml.XmlNode>()
|
||||
.Where(childnode => childnode.Attributes["key"].Value == "title")
|
||||
.Select(childnode => childnode.Attributes["value"].Value)
|
||||
.FirstOrDefault();
|
||||
|
||||
AddChapter(new Chapter(startTime, endTime, chapterTitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="taglib-sharp">
|
||||
<HintPath>lib\taglib-sharp.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
@@ -1,339 +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; }
|
||||
|
||||
private Func<Task<string>> getKeyFuncAsync { get; }
|
||||
|
||||
public static async Task<AaxToM4bConverter> CreateAsync(string inputFile, string decryptKey, Func<Task<string>> getKeyFunc, Chapters chapters = null)
|
||||
{
|
||||
var converter = new AaxToM4bConverter(inputFile, decryptKey, getKeyFunc);
|
||||
converter.chapters = chapters ?? new AAXChapters(inputFile);
|
||||
await converter.prelimProcessing();
|
||||
converter.printPrelim();
|
||||
|
||||
return converter;
|
||||
}
|
||||
private AaxToM4bConverter(string inputFile, string decryptKey, Func<Task<string>> getKeyFunc)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(inputFile, nameof(inputFile));
|
||||
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
|
||||
};
|
||||
|
||||
inputFileName = inputFile;
|
||||
this.decryptKey = decryptKey;
|
||||
this.getKeyFuncAsync = getKeyFunc;
|
||||
}
|
||||
|
||||
private async Task prelimProcessing()
|
||||
{
|
||||
tags = new Tags(inputFileName);
|
||||
encodingInfo = new EncodingInfo(inputFileName);
|
||||
|
||||
var defaultFilename = Path.Combine(
|
||||
Path.GetDirectoryName(inputFileName),
|
||||
PathLib.ToPathSafeString(tags.author),
|
||||
PathLib.ToPathSafeString(tags.title) + ".m4b"
|
||||
);
|
||||
|
||||
// set default name
|
||||
SetOutputFilename(defaultFilename);
|
||||
|
||||
await Task.Run(() => saveCover(inputFileName));
|
||||
}
|
||||
|
||||
private void saveCover(string aaxFile)
|
||||
{
|
||||
using var file = TagLib.File.Create(aaxFile, "audio/mp4", TagLib.ReadStyle.Average);
|
||||
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)
|
||||
{
|
||||
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
|
||||
outDir = Path.GetDirectoryName(outputFileName);
|
||||
|
||||
if (File.Exists(outputFileName))
|
||||
File.Delete(outputFileName);
|
||||
}
|
||||
|
||||
private string outputFileWithNewExt(string extension) => PathLib.ReplaceExtension(outputFileName, extension);
|
||||
|
||||
public bool Step1_CreateDir()
|
||||
{
|
||||
ProcessRunner.WorkingDir = outDir;
|
||||
Directory.CreateDirectory(outDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step2_DecryptAax()
|
||||
{
|
||||
DecryptProgressUpdate?.Invoke(this, 0);
|
||||
|
||||
var tempRipFile = Path.Combine(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}");
|
||||
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)
|
||||
{
|
||||
decryptKey = getKey();
|
||||
return decrypt(tempRipFile);
|
||||
}
|
||||
|
||||
// I am NOT happy about doing async this way. Async needs to be added to Step framework
|
||||
string getKey() => getKeyFuncAsync().GetAwaiter().GetResult();
|
||||
|
||||
private int decrypt(string tempRipFile)
|
||||
{
|
||||
FileExt.SafeDelete(tempRipFile);
|
||||
|
||||
Console.WriteLine("Decrypting with key " + decryptKey);
|
||||
|
||||
var returnCode = 100;
|
||||
var thread = new Thread(() => returnCode = ngDecrypt());
|
||||
thread.Start();
|
||||
|
||||
double fileLen = new FileInfo(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 " + encodingInfo.channels + " -r " + encodingInfo.sampleRate + " \"" + inputFileName + "\""
|
||||
};
|
||||
info.EnvironmentVariables["VARIABLE"] = decryptKey;
|
||||
|
||||
var result = info.RunHidden();
|
||||
|
||||
// bad checksum -- bad decrypt key
|
||||
if (result.Output.Contains("checksums mismatch, aborting!"))
|
||||
return -99;
|
||||
|
||||
return result.ExitCode;
|
||||
}
|
||||
|
||||
// temp file names for steps 3, 4, 5
|
||||
string tempChapsGuid { get; } = Guid.NewGuid().ToString().ToUpper().Replace("-", "");
|
||||
string tempChapsPath => Path.Combine(outDir, $"tempChaps_{tempChapsGuid}.mp4");
|
||||
string mp4_file => outputFileWithNewExt(".mp4");
|
||||
string ff_txt_file => mp4_file + ".ff.txt";
|
||||
|
||||
public bool Step3_Chapterize()
|
||||
{
|
||||
var str1 = "";
|
||||
if (chapters.FirstChapter.StartTime != 0.0)
|
||||
{
|
||||
str1 = " -ss " + chapters.FirstChapter.StartTime.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + chapters.LastChapter.EndTime.ToString("0.000", CultureInfo.InvariantCulture) + " ";
|
||||
}
|
||||
|
||||
var ffmpegTags = tags.GenerateFfmpegTags();
|
||||
var ffmpegChapters = 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(outDir, "cover-" + Guid.NewGuid() + ".jpg");
|
||||
FileExt.CreateFile(coverPath, 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, outputFileName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step6_AddTags()
|
||||
{
|
||||
tags.AddAppleTags(outputFileName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool End_CreateCue()
|
||||
{
|
||||
File.WriteAllText(outputFileWithNewExt(".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), chapters));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool End_CreateNfo()
|
||||
{
|
||||
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateContents(AppName, tags, encodingInfo, chapters));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class Chapter
|
||||
{
|
||||
public Chapter(double startTime, double endTime, string title)
|
||||
{
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Title = title;
|
||||
}
|
||||
/// <summary>
|
||||
/// Chapter start time, in seconds.
|
||||
/// </summary>
|
||||
public double StartTime { get; private set; }
|
||||
/// <summary>
|
||||
/// Chapter end time, in seconds.
|
||||
/// </summary>
|
||||
public double EndTime { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public abstract class Chapters
|
||||
{
|
||||
private List<Chapter> _chapterList = new();
|
||||
public int Count => _chapterList.Count;
|
||||
public Chapter FirstChapter => _chapterList[0];
|
||||
public Chapter LastChapter => _chapterList[Count - 1];
|
||||
public IEnumerable<Chapter> ChapterList => _chapterList.AsEnumerable();
|
||||
public IEnumerable<TimeSpan> GetBeginningTimes() => ChapterList.Select(c => TimeSpan.FromSeconds(c.StartTime));
|
||||
protected void AddChapter(Chapter chapter)
|
||||
{
|
||||
_chapterList.Add(chapter);
|
||||
}
|
||||
protected void AddChapters(IEnumerable<Chapter> chapters)
|
||||
{
|
||||
_chapterList.AddRange(chapters);
|
||||
}
|
||||
public string GenerateFfmpegChapters()
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
|
||||
foreach (Chapter c in ChapterList)
|
||||
{
|
||||
stringBuilder.Append("[CHAPTER]\n");
|
||||
stringBuilder.Append("TIMEBASE=1/1000\n");
|
||||
stringBuilder.Append("START=" + c.StartTime * 1000 + "\n");
|
||||
stringBuilder.Append("END=" + c.EndTime * 1000 + "\n");
|
||||
stringBuilder.Append("title=" + c.Title + "\n");
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public static class NFO
|
||||
{
|
||||
public static string CreateContents(string ripper, Tags tags, EncodingInfo encodingInfo, Chapters chapters)
|
||||
{
|
||||
var _hours = (int)tags.duration.TotalHours;
|
||||
var myDuration
|
||||
= (_hours > 0 ? _hours + " hours, " : "")
|
||||
+ tags.duration.Minutes + " minutes, "
|
||||
+ tags.duration.Seconds + " seconds";
|
||||
|
||||
var header
|
||||
= "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 != "")
|
||||
header += $" Genre: {tags.genre}\r\n";
|
||||
|
||||
var s
|
||||
= header
|
||||
+ $" 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
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; }
|
||||
|
||||
// input file
|
||||
public Tags(string file)
|
||||
{
|
||||
using var tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
|
||||
title = tagLibFile.Tag.Title.Replace(" (Unabridged)", "");
|
||||
album = tagLibFile.Tag.Album.Replace(" (Unabridged)", "");
|
||||
author = tagLibFile.Tag.FirstPerformer ?? "[unknown]";
|
||||
year = tagLibFile.Tag.Year.ToString();
|
||||
comments = tagLibFile.Tag.Comment ?? "";
|
||||
duration = tagLibFile.Properties.Duration;
|
||||
genre = tagLibFile.Tag.FirstGenre ?? "";
|
||||
|
||||
var tag = tagLibFile.GetTag(TagTypes.Apple, true);
|
||||
publisher = tag.Publisher ?? "";
|
||||
narrator = string.IsNullOrWhiteSpace(tagLibFile.Tag.FirstComposer) ? tag.Narrator : tagLibFile.Tag.FirstComposer;
|
||||
comments = !string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description;
|
||||
id = tag.AudibleCDEK;
|
||||
}
|
||||
|
||||
// my best guess of what this step is doing:
|
||||
// re-publish the data we read from the input file => output file
|
||||
public void AddAppleTags(string file)
|
||||
{
|
||||
using var tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
|
||||
var tag = (AppleTag)tagLibFile.GetTag(TagTypes.Apple, true);
|
||||
tag.Publisher = publisher;
|
||||
tag.LongDescription = comments;
|
||||
tag.Description = comments;
|
||||
tagLibFile.Save();
|
||||
}
|
||||
|
||||
public string GenerateFfmpegTags()
|
||||
=> $";FFMETADATA1"
|
||||
+ $"\nmajor_brand=aax"
|
||||
+ $"\nminor_version=1"
|
||||
+ $"\ncompatible_brands=aax M4B mp42isom"
|
||||
+ $"\ndate={year}"
|
||||
+ $"\ngenre={genre}"
|
||||
+ $"\ntitle={title}"
|
||||
+ $"\nartist={author}"
|
||||
+ $"\nalbum={album}"
|
||||
+ $"\ncomposer={narrator}"
|
||||
+ $"\ncomment={comments.Truncate(254)}"
|
||||
+ $"\ndescription={comments}"
|
||||
+ $"\n";
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="27.0.2" />
|
||||
<PackageReference Include="NPOI" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
|
||||
<ProjectReference Include="..\DtoImporterService\DtoImporterService.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
<ProjectReference Include="..\LibationSearchEngine\LibationSearchEngine.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class DbContexts
|
||||
{
|
||||
//// idea for future command/query separation
|
||||
// public static LibationContext GetCommandContext() { }
|
||||
// public static LibationContext GetQueryContext() { }
|
||||
|
||||
public static LibationContext GetContext()
|
||||
=> LibationContext.Create(SqliteStorage.ConnectionString);
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using DtoImporterService;
|
||||
using InternalUtilities;
|
||||
using Serilog;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class LibraryCommands
|
||||
{
|
||||
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.MoveResponseBodyFile(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.ResponseBodyFilePath
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
{
|
||||
internal class LibraryBookConfig : IEntityTypeConfiguration<LibraryBook>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<LibraryBook> entity)
|
||||
{
|
||||
entity.HasKey(b => b.BookId);
|
||||
|
||||
entity
|
||||
.HasOne(le => le.Book)
|
||||
.WithOne()
|
||||
.HasForeignKey<LibraryBook>(le => le.BookId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace DataLayer
|
||||
{
|
||||
public enum Role { Author = 1, Narrator = 2, Publisher = 3 }
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
public class SeriesBook
|
||||
{
|
||||
internal int SeriesId { get; private set; }
|
||||
internal int BookId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>"index" not "order". This is both for sequence and display</para>
|
||||
/// <para>Float allows for in-between books. eg: 2.5</para>
|
||||
/// <para>To show 2 editions as the same book in a series, give them the same index</para>
|
||||
/// <para>null IS NOT the same as 0. Some series call a book "book 0"</para>
|
||||
/// </summary>
|
||||
public float? Index { get; private set; }
|
||||
|
||||
public Series Series { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
|
||||
private SeriesBook() { }
|
||||
internal SeriesBook(Series series, Book book, float? index = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
|
||||
Series = series;
|
||||
Book = book;
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public void UpdateIndex(float? index)
|
||||
{
|
||||
if (index.HasValue)
|
||||
Index = index.Value;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Series={Series} Book={Book}";
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
public static class GenericPaging
|
||||
{
|
||||
public static IQueryable<T> Page<T>(this IQueryable<T> query, int pageNumZeroStart, int pageSize)
|
||||
{
|
||||
if (pageSize < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(pageSize), "pageSize must be at least 1");
|
||||
|
||||
if (pageNumZeroStart > 0)
|
||||
query = query.Skip(pageNumZeroStart * pageSize);
|
||||
|
||||
return query.Take(pageSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer.Utilities
|
||||
{
|
||||
public static class LocalDatabaseInfo
|
||||
{
|
||||
public static List<string> GetLocalDBInstances()
|
||||
{
|
||||
// Start the child process.
|
||||
using var p = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
FileName = "cmd.exe",
|
||||
Arguments = "/C sqllocaldb info",
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
|
||||
}
|
||||
};
|
||||
p.Start();
|
||||
var output = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit();
|
||||
|
||||
// if LocalDb is not installed then it will return that 'sqllocaldb' is not recognized as an internal or external command operable program or batch file
|
||||
return string.IsNullOrWhiteSpace(output) || output.Contains("not recognized")
|
||||
? new List<string>()
|
||||
: output
|
||||
.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
|
||||
.Select(i => i.Trim())
|
||||
.Where(i => !string.IsNullOrEmpty(i))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
FOR QUICK MIGRATION INSTRUCTIONS:
|
||||
_DB_NOTES.txt
|
||||
|
||||
|
||||
HOW TO CREATE: EF CORE PROJECT
|
||||
==============================
|
||||
example is for sqlite but the same works with MsSql
|
||||
|
||||
|
||||
nuget
|
||||
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
|
||||
Microsoft.EntityFrameworkCore.Sqlite
|
||||
|
||||
MIGRATIONS
|
||||
require core, not standard
|
||||
this can be a problem b/c standard and framework can only reference standard, not core
|
||||
TO USE MIGRATIONS (core and/or standard)
|
||||
add to csproj
|
||||
<PropertyGroup>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
</PropertyGroup>
|
||||
TO USE MIGRATIONS AS *BOTH* CORE AND STANDARD
|
||||
edit csproj
|
||||
pluralize this xml tag
|
||||
from: TargetFramework
|
||||
to: TargetFrameworks
|
||||
inside of TargetFrameworks
|
||||
from: netstandard2.1
|
||||
to: netcoreapp3.1;netstandard2.1
|
||||
|
||||
run. error
|
||||
SQLite Error 1: 'no such table: Blogs'.
|
||||
|
||||
set project "Set as StartUp Project"
|
||||
|
||||
Tools >> Nuget Package Manager >> Package Manager Console
|
||||
default project: Examples\SQLite_NETCore2_0
|
||||
|
||||
PM> add-migration InitialCreate
|
||||
PM> Update-Database
|
||||
|
||||
if add-migration xyz throws and error, don't take the error msg at face value. try again with add-migration xyz -verbose
|
||||
|
||||
new sqlite .db file created: Copy always/Copy if newer
|
||||
or copy .db file to destination
|
||||
|
||||
relative:
|
||||
optionsBuilder.UseSqlite("Data Source=blogging.db");
|
||||
absolute (use fwd slashes):
|
||||
optionsBuilder.UseSqlite("Data Source=C:/foo/bar/blogging.db");
|
||||
|
||||
|
||||
REFERENCE ARTICLES
|
||||
------------------
|
||||
https://docs.microsoft.com/en-us/ef/core/get-started/netcore/new-db-sqlite
|
||||
https://carlos.mendible.com/2016/07/11/step-by-step-dotnet-core-and-entity-framework-core/
|
||||
https://www.benday.com/2017/12/19/ef-core-2-0-migrations-without-hard-coded-connection-strings/
|
||||
79
Documentation/Advanced.md
Normal file
@@ -0,0 +1,79 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
# Advanced: Table of Contents
|
||||
|
||||
- [Files and folders](#files-and-folders)
|
||||
- [Linux and Mac (unofficial)](#linux-and-mac)
|
||||
- [Settings](#settings)
|
||||
- [Custom File Naming](#custom-file-naming)
|
||||
- [Command Line Interface](#command-line-interface)
|
||||
|
||||
|
||||
|
||||
### Files and folders
|
||||
|
||||
To make upgrades and reinstalls easier, Libation separates all of its responsibilities to a few different folders. If you don't want to mess with this stuff: ignore it. Read on if you like a little more control over your files.
|
||||
|
||||
* In Libation's initial folder are the files that make up the program. Since nothing else is here, just copy new files here to upgrade the program. Delete this folder to delete Libation.
|
||||
|
||||
* In a separate folder, Libation keeps track of all of the files it creates like settings and downloaded images. After an upgrade, Libation might think that's its being run for the first time. Just click ADVANCED SETUP and point to this folder. Libation will reload your library and settings.
|
||||
|
||||
* The last important folder is the "books location." This is where Libation looks for your downloaded and decrypted books. This is how it knows which books still need to be downloaded. The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
|
||||
|
||||
### Linux and Mac
|
||||
|
||||
Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592))
|
||||
|
||||
### Settings
|
||||
|
||||
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
|
||||
|
||||
### Custom File Naming
|
||||
|
||||
In Settings, on the Download/Decrypt tab, you can specify the format in which you want your files to be named. As you edit these templates, a live example will be shown. Parameters are listed for folders, files, and files split by chapter including an explanation of what each naming option means. For instance: you can use template `<title short> - <ch# 0> of <ch count> - <ch title>` to create the file `A Study in Scarlet - 04 of 10 - A Flight for Life.m4b`.
|
||||
|
||||
These templates apply to GUI and CLI.
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
Libationcli.exe allows limited access to Libation's functionalities as a CLI.
|
||||
|
||||
Warnings about relying solely on on the CLI:
|
||||
* CLI will not perform any upgrades.
|
||||
* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
|
||||
* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
|
||||
|
||||
```
|
||||
help
|
||||
libationcli --help
|
||||
|
||||
verb-specific help
|
||||
libationcli scan --help
|
||||
|
||||
scan all libraries
|
||||
libationcli scan
|
||||
scan only libraries for specific accounts
|
||||
libationcli scan nickname1 nickname2
|
||||
|
||||
convert all m4b files to mp3
|
||||
libationcli convert
|
||||
|
||||
liberate all books and pdfs
|
||||
libationcli liberate
|
||||
liberate pdfs only
|
||||
libationcli liberate --pdf
|
||||
libationcli liberate -p
|
||||
|
||||
export library to file
|
||||
libationcli export --path "C:\foo\bar\my.json" --json
|
||||
libationcli export -p "C:\foo\bar\my.json" -j
|
||||
libationcli export -p "C:\foo\bar\my.csv" --csv
|
||||
libationcli export -p "C:\foo\bar\my.csv" -c
|
||||
libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
|
||||
libationcli export -p "C:\foo\bar\my.xlsx" -x
|
||||
```
|
||||
138
Documentation/GettingStarted.md
Normal file
@@ -0,0 +1,138 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
# Getting started: Table of Contents
|
||||
|
||||
- [Download Libation](#download-libation-1)
|
||||
- [Installation](#installation)
|
||||
- [Create Accounts](#create-accounts)
|
||||
- [Import your library](#import-your-library)
|
||||
- [Download your books -- DRM-free!](#download-your-books----drm-free)
|
||||
- [Download PDF attachments](#download-pdf-attachments)
|
||||
- [Details of downloaded files](#details-of-downloaded-files)
|
||||
- [Export your library](#export-your-library)
|
||||
|
||||
|
||||
|
||||
### [Download Libation](https://github.com/rmcrackan/Libation/releases)
|
||||
|
||||
### Installation
|
||||
|
||||
To install Libation, extract the zip file to a folder, for example `C:\Libation`, and then run Libation.exe from that folder to begin the configuration process and configure your account(s).
|
||||
|
||||
### Create Accounts
|
||||
|
||||
Create your account(s):
|
||||
|
||||

|
||||
|
||||
New locale options include many more regions including old audible accounts which pre-date the amazon acquisition
|
||||
|
||||

|
||||
|
||||
### Import your library
|
||||
|
||||
Be default, Libation will periodically scan the accounts you added above with a checkbox next to them. Nothing for you to do. You can also scan manually.
|
||||
|
||||
Select Import > Scan Library:
|
||||
|
||||

|
||||
|
||||
Or if you have multiple accounts, you'll get to choose whether to scan all accounts or just the ones you select:
|
||||
|
||||

|
||||
|
||||
If this is a new installation, or you're scanning an account you haven't scanned before, you'll be prompted to enter your password for the Audible account.
|
||||
|
||||

|
||||
|
||||
Enter the password and click Submit. Audible will prompt you with a CAPTCHA image.
|
||||
|
||||

|
||||
|
||||
Enter the CAPTCHA answer characters and click Submit. If all has gone well, Libation will start scanning the account.
|
||||
|
||||
In rare instances, the Captcha image/response will fail in an endless loop. If this happens, delete the problem account, and then click Save. Re-add the account and click Save again. Now try to scan the account again. This time, instead of typing your password, click the link that says "Or click here". This will open the Audible External Login dialog shown below.
|
||||
|
||||

|
||||
|
||||
You can either copy the URL shown and paste it into your browser or launch the browser directly by clicking Launch in Browser. Audible will display its standard login page. Login, including answering the CAPTCHA on the next page. In some cases, you might have to approve the login from the email account associated with that login, but once the login is successful, you'll see an error message.
|
||||
|
||||

|
||||
|
||||
This actually means you've successfully logged in. Copy the entire URL shown in your browser and return to Libation. Paste that URL into the text box at the bottom of the Audible External Login window and click Submit.
|
||||
|
||||
You'll see this window while it's scanning:
|
||||
|
||||

|
||||
|
||||
Success! We see how many new titles are imported:
|
||||
|
||||

|
||||
|
||||
### Download your books -- DRM-free!
|
||||
|
||||
Automatically download some or all of your audible books. This shows you how much of your library is not yet downloaded and decrypted:
|
||||
|
||||
The stoplights will tell you a title's status:
|
||||
|
||||
* Green: downloaded and decrypted
|
||||
* Yellow: downloaded but still encrypted with DRM
|
||||
* Red: not downloaded
|
||||
* PDF icon without arrow: downloaded
|
||||
* PDF with arrow: not downloaded
|
||||
|
||||
Or hover over the button to see the status.
|
||||
|
||||

|
||||
|
||||
Select Liberate > Begin Book Backups
|
||||
|
||||
You can also click on the stop light to download only that title and its PDF
|
||||
|
||||

|
||||
|
||||
First the original book with DRM is downloaded
|
||||
|
||||

|
||||
|
||||
Then it's decrypted so you can use it on any device you choose. The very first time you decrypt a book, this step will take a while. Every other book will go much faster. The first time, Libation has to figure out the special decryption key which allows your personal books to be unlocked.
|
||||
|
||||

|
||||
|
||||
And voila! If you have multiple books not yet liberated, Libation will automatically move on to the next.
|
||||
|
||||

|
||||
|
||||
The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
|
||||
|
||||
### Download PDF attachments
|
||||
|
||||
For books which include PDF downloads, Libation can download these for you as well and will attempt to store them with the book. "Book backup" will already download an available PDF. This additional option is useful when Audible adds a PDF to your book after you've already backed it up.
|
||||
|
||||
Select Liberate > Begin PDF Backups
|
||||
|
||||

|
||||
|
||||
The downloads work just like with books, only with no additional decryption needed.
|
||||
|
||||

|
||||
|
||||
### Details of downloaded files
|
||||
|
||||

|
||||
|
||||
When you set up Libation, you'll specify a Books directory. Libation looks inside that directory and all subdirectories to look for files or folders with each library book's audible id. This way, organization is completely up to you. When you download + decrypt a book, you get several files
|
||||
|
||||
* .m4b: your audiobook in m4b format. This is the most pure version of your audiobook and retains the highest quality. Now that it's decrypted, you can play it on any audio player and put it on any device. If you'd like, you can also use 3rd party tools to turn it into an mp3. The freedom to do what you want with your files was the original inspiration for Libation.
|
||||
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
|
||||
|
||||
### Export your library
|
||||
|
||||

|
||||
|
||||
Export your library to Excel, CSV, or JSON
|
||||
79
Documentation/SearchingAndFiltering.md
Normal file
@@ -0,0 +1,79 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
# Searching and filtering: Table of Contents
|
||||
|
||||
- [Tags](#tags)
|
||||
- [Searches](#searches)
|
||||
- [Search examples](#search-examples)
|
||||
- [Filters](#filters)
|
||||
|
||||
|
||||
|
||||
### Tags
|
||||
|
||||
To add tags to a title, click the tags button
|
||||
|
||||

|
||||
|
||||
Add as many tags as you'd like. Tags are separated by a space. Each tag can contain letters, numbers, and underscores
|
||||
|
||||

|
||||
|
||||
Tags are saved non-case specific for easy search. There is one special tag "hidden" which will also grey-out the book
|
||||
|
||||

|
||||
|
||||
To edit tags, just click the button again.
|
||||
|
||||
### Searches
|
||||
|
||||
Libation's advanced searching is built on the powerful Lucene search engine. Simple searches are effortless and powerful searches are simple. To search, just type and click Filter or press enter
|
||||
|
||||
* Type anything in the search box to search common fields: title, authors, narrators, and the book's audible id
|
||||
* Use Lucene's "Query Parser Syntax" for advanced searching.
|
||||
* Easy tutorial: http://www.lucenetutorial.com/lucene-query-syntax.html
|
||||
* Full official guide: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html
|
||||
* Tons of search fields, specific to audiobooks
|
||||
* Synonyms so you don't have to memorize magic words. Eg: author and author**s** will both work
|
||||
* Click [?] button for a full list of search fields and synonyms 
|
||||
* Search by tag like \[this\]
|
||||
* When tags have an underscore you can use part of the tag. This is useful for quick categories. The below examples make this more clear.
|
||||
|
||||
### Search examples
|
||||
|
||||
Search for anything with the word potter
|
||||
|
||||

|
||||
|
||||
If you only want to see Harry Potter
|
||||
|
||||

|
||||
|
||||
If you only want to see potter except for Harry Potter. You can also use "-" instead of "NOT"
|
||||
|
||||

|
||||

|
||||
|
||||
To see only books written by Neil Gaiman where he also narrates his own book. (If you don't include AND, you'll see everything written by Neil Gaiman and also all books in your library which are self-narrated.)
|
||||
|
||||

|
||||
|
||||
I tagged autobiographies as auto_bio and biographies written by someone else as bio. I can get only autobiographies with \[auto_bio\] or get both by searching \[bio\]
|
||||
|
||||
![Search example: \[bio\]](images/SearchExampleBio.png)
|
||||
![Search example: \[auto_bio\]](images/SearchExampleAutoBio.png)
|
||||
|
||||
### Filters
|
||||
|
||||
If you have a search you want to save, click Add To Quick Filters to save it in your Quick Filters list. To use it again, select it from the Quick Filters list.
|
||||
|
||||
To edit this list go to Quick Filters > Edit quick filters. Here you can re-order the list, delete filters, double-click a filter to edit it, or double-click the bottom blank box to add a new filter.
|
||||
|
||||
Check "Quick Filters > Start Libation with 1st filter Default" to have your top filter automatically applied when Libation starts. In this top example, I want to always start without these: at books I've tagged hidden, books I've tagged as free_audible_originals, and books which I have rated.
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
BIN
Documentation/images/Import2.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
BIN
Documentation/images/alt-login1.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
Documentation/images/alt-login2.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
Documentation/images/alt-login3.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
Documentation/images/alt-login4.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,102 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
public class ContributorImporter : ItemsImporterBase
|
||||
{
|
||||
public ContributorImporter(LibationContext context) : base(context) { }
|
||||
|
||||
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new ContributorValidator().Validate(importItems.Select(i => i.DtoItem));
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// get distinct
|
||||
var authors = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetAuthorsDistinct()
|
||||
.ToList();
|
||||
var narrators = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetNarratorsDistinct()
|
||||
.ToList();
|
||||
var publishers = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetPublishersDistinct()
|
||||
.ToList();
|
||||
|
||||
// load db existing => .Local
|
||||
var allNames = publishers
|
||||
.Union(authors.Select(n => n.Name))
|
||||
.Union(narrators.Select(n => n.Name))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.ToList();
|
||||
loadLocal_contributors(allNames);
|
||||
|
||||
// upsert
|
||||
var qtyNew = 0;
|
||||
qtyNew += upsertPeople(authors);
|
||||
qtyNew += upsertPeople(narrators);
|
||||
qtyNew += upsertPublishers(publishers);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private void loadLocal_contributors(List<string> contributorNames)
|
||||
{
|
||||
//// BAD: very inefficient
|
||||
// var x = context.Contributors.Local.Where(c => !contribNames.Contains(c.Name));
|
||||
|
||||
// GOOD: Except() is efficient. Due to hashing, it's close to O(n)
|
||||
var localNames = DbContext.Contributors.Local.Select(c => c.Name);
|
||||
var remainingContribNames = contributorNames
|
||||
.Distinct()
|
||||
.Except(localNames)
|
||||
.ToList();
|
||||
|
||||
// load existing => local
|
||||
// remember to include default/empty/missing
|
||||
var emptyName = Contributor.GetEmpty().Name;
|
||||
if (remainingContribNames.Any())
|
||||
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name) || c.Name == emptyName).ToList();
|
||||
}
|
||||
|
||||
// only use after loading contributors => local
|
||||
private int upsertPeople(List<Person> people)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var p in people)
|
||||
{
|
||||
var person = DbContext.Contributors.Local.SingleOrDefault(c => c.Name == p.Name);
|
||||
if (person == null)
|
||||
{
|
||||
person = DbContext.Contributors.Add(new Contributor(p.Name, p.Asin)).Entity;
|
||||
qtyNew++;
|
||||
}
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
// only use after loading contributors => local
|
||||
private int upsertPublishers(List<string> publishers)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var publisherName in publishers)
|
||||
{
|
||||
if (DbContext.Contributors.Local.SingleOrDefault(c => c.Name == publisherName) == null)
|
||||
{
|
||||
DbContext.Contributors.Add(new Contributor(publisherName));
|
||||
qtyNew++;
|
||||
}
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
public class LibraryImporter : ItemsImporterBase
|
||||
{
|
||||
public LibraryImporter(LibationContext context) : base(context) { }
|
||||
|
||||
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new LibraryValidator().Validate(importItems.Select(i => i.DtoItem));
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
new BookImporter(DbContext).Import(importItems);
|
||||
|
||||
var qtyNew = upsertLibraryBooks(importItems);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private int upsertLibraryBooks(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// technically, we should be able to have duplicate books from separate accounts.
|
||||
// this would violate the current pk and would be difficult to deal with elsewhere:
|
||||
// - what to show in the grid
|
||||
// - which to consider liberated
|
||||
//
|
||||
// sqlite cannot alter pk. the work around is an extensive headache. it'll be fixed in pre .net5/efcore5
|
||||
//
|
||||
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
var currentLibraryProductIds = DbContext.Library.Select(l => l.Book.AudibleProductId).ToList();
|
||||
var newItems = importItems.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId)).ToList();
|
||||
|
||||
foreach (var newItem in newItems)
|
||||
{
|
||||
var libraryBook = new LibraryBook(
|
||||
DbContext.Books.Local.Single(b => b.AudibleProductId == newItem.DtoItem.ProductId),
|
||||
newItem.DtoItem.DateAdded,
|
||||
newItem.AccountId);
|
||||
DbContext.Library.Add(libraryBook);
|
||||
}
|
||||
|
||||
// needed for v3 => v4 upgrade
|
||||
var toUpdate = DbContext.Library.Where(l => l.Account == null);
|
||||
foreach (var u in toUpdate)
|
||||
{
|
||||
var item = importItems.FirstOrDefault(ii => ii.DtoItem.ProductId == u.Book.AudibleProductId);
|
||||
if (item != null)
|
||||
u.UpdateAccount(item.AccountId);
|
||||
}
|
||||
|
||||
var qtyNew = newItems.Count;
|
||||
return qtyNew;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
public class SeriesImporter : ItemsImporterBase
|
||||
{
|
||||
public SeriesImporter(LibationContext context) : base(context) { }
|
||||
|
||||
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new SeriesValidator().Validate(importItems.Select(i => i.DtoItem));
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// get distinct
|
||||
var series = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetSeriesDistinct()
|
||||
.ToList();
|
||||
|
||||
// load db existing => .Local
|
||||
loadLocal_series(series);
|
||||
|
||||
// upsert
|
||||
var qtyNew = upsertSeries(series);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private void loadLocal_series(List<AudibleApiDTOs.Series> series)
|
||||
{
|
||||
var seriesIds = series.Select(s => s.SeriesId).ToList();
|
||||
var localIds = DbContext.Series.Local.Select(s => s.AudibleSeriesId).ToList();
|
||||
var remainingSeriesIds = seriesIds
|
||||
.Distinct()
|
||||
.Except(localIds)
|
||||
.ToList();
|
||||
|
||||
if (remainingSeriesIds.Any())
|
||||
DbContext.Series.Where(s => remainingSeriesIds.Contains(s.AudibleSeriesId)).ToList();
|
||||
}
|
||||
|
||||
private int upsertSeries(List<AudibleApiDTOs.Series> requestedSeries)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var s in requestedSeries)
|
||||
{
|
||||
var series = DbContext.Series.Local.SingleOrDefault(c => c.AudibleSeriesId == s.SeriesId);
|
||||
if (series is null)
|
||||
{
|
||||
series = DbContext.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity;
|
||||
qtyNew++;
|
||||
}
|
||||
series.UpdateName(s.SeriesName);
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book and decrypt audiobook files
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
/// Decrypt: remove DRM encryption from audiobook. Store final book
|
||||
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
|
||||
/// </summary>
|
||||
public class BackupBook : IProcessable
|
||||
{
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public DownloadBook DownloadBook { get; } = new DownloadBook();
|
||||
public DecryptBook DecryptBook { get; } = new DecryptBook();
|
||||
public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessAsync()
|
||||
// often calls events which prints to forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
var statusHandler = await DownloadBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
{
|
||||
var statusHandler = await DecryptBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
{
|
||||
var statusHandler = await DownloadPdf.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Decrypt audiobook files
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
/// Decrypt: remove DRM encryption from audiobook. Store final book
|
||||
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
|
||||
/// </summary>
|
||||
public class DecryptBook : IDecryptable
|
||||
{
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string> DecryptBegin;
|
||||
|
||||
public event EventHandler<string> TitleDiscovered;
|
||||
public event EventHandler<string> AuthorsDiscovered;
|
||||
public event EventHandler<string> NarratorsDiscovered;
|
||||
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
|
||||
public event EventHandler<int> UpdateProgress;
|
||||
|
||||
public event EventHandler<string> DecryptCompleted;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
|
||||
&& !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessAsync()
|
||||
// often calls events which prints to forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
try
|
||||
{
|
||||
var aaxFilename = AudibleFileStorage.AAX.GetPath(libraryBook.Book.AudibleProductId);
|
||||
|
||||
if (aaxFilename == null)
|
||||
return new StatusHandler { "aaxFilename parameter is null" };
|
||||
if (!File.Exists(aaxFilename))
|
||||
return new StatusHandler { $"Cannot find AAX file: {aaxFilename}" };
|
||||
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
var api = await AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
var chapters = await downloadChapterNames(libraryBook, api);
|
||||
|
||||
var outputAudioFilename = await aaxToM4bConverterDecrypt(aaxFilename, libraryBook, chapters, api);
|
||||
|
||||
// decrypt failed
|
||||
if (outputAudioFilename == null)
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
|
||||
var destinationDir = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
|
||||
var config = Configuration.Instance;
|
||||
if (config.RetainAaxFiles)
|
||||
{
|
||||
var newAaxFilename = FileUtility.GetValidFilename(
|
||||
destinationDir,
|
||||
Path.GetFileNameWithoutExtension(aaxFilename),
|
||||
"aax");
|
||||
File.Move(aaxFilename, newAaxFilename);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
|
||||
}
|
||||
|
||||
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
if (!finalAudioExists)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Chapters> downloadChapterNames(LibraryBook libraryBook, Api api)
|
||||
{
|
||||
try
|
||||
{
|
||||
var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId);
|
||||
if (contentMetadata?.ChapterInfo is null)
|
||||
return null;
|
||||
|
||||
return new DownloadedChapters(contentMetadata.ChapterInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> aaxToM4bConverterDecrypt(string aaxFilename, LibraryBook libraryBook, Chapters chapters, Api api)
|
||||
{
|
||||
DecryptBegin?.Invoke(this, $"Begin decrypting {aaxFilename}");
|
||||
|
||||
try
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
var account = persister
|
||||
.AccountsSettings
|
||||
.GetAccount(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, account.DecryptKey, api.GetActivationBytesAsync, chapters);
|
||||
converter.AppName = "Libation";
|
||||
|
||||
TitleDiscovered?.Invoke(this, converter.tags.title);
|
||||
AuthorsDiscovered?.Invoke(this, converter.tags.author);
|
||||
NarratorsDiscovered?.Invoke(this, converter.tags.narrator);
|
||||
CoverImageFilepathDiscovered?.Invoke(this, converter.coverBytes);
|
||||
|
||||
// override default which was set in CreateAsync
|
||||
var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
|
||||
converter.SetOutputFilename(proposedOutputFile);
|
||||
converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await Task.Run(() => converter.Run());
|
||||
|
||||
// decrypt failed
|
||||
if (!success)
|
||||
return null;
|
||||
|
||||
account.DecryptKey = converter.decryptKey;
|
||||
|
||||
return converter.outputFileName;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DecryptCompleted?.Invoke(this, $"Completed decrypting {aaxFilename}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
{
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
|
||||
|
||||
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
|
||||
|
||||
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
|
||||
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
|
||||
|
||||
foreach (var f in sortedFiles)
|
||||
{
|
||||
var dest
|
||||
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
||||
? audioFileName
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
|
||||
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||
|
||||
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
|
||||
Cue.UpdateFileName(f, audioFileName);
|
||||
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
|
||||
{
|
||||
// files are: temp path\author\[asin].ext
|
||||
var m4bDir = new FileInfo(outputAudioFilename).Directory;
|
||||
var files = m4bDir
|
||||
.EnumerateFiles()
|
||||
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
|
||||
.ToList();
|
||||
|
||||
// move audio files to the end of the collection so these files are moved last
|
||||
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
|
||||
var sortedFiles = files
|
||||
.Except(musicFiles)
|
||||
.Concat(musicFiles)
|
||||
.ToList();
|
||||
|
||||
return sortedFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
/// Decrypt: remove DRM encryption from audiobook. Store final book
|
||||
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
|
||||
/// </summary>
|
||||
public class DownloadBook : DownloadableBase
|
||||
{
|
||||
private const string SERVICE_UNAVAILABLE = "Content Delivery Companion Service is not available.";
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)
|
||||
&& !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId);
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var tempAaxFilename = getDownloadPath(libraryBook);
|
||||
var actualFilePath = await downloadBookAsync(libraryBook, tempAaxFilename);
|
||||
moveBook(libraryBook, actualFilePath);
|
||||
return verifyDownload(libraryBook);
|
||||
}
|
||||
|
||||
private static string getDownloadPath(LibraryBook libraryBook)
|
||||
=> FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.DownloadsInProgress,
|
||||
libraryBook.Book.Title,
|
||||
"aax",
|
||||
libraryBook.Book.AudibleProductId);
|
||||
|
||||
private async Task<string> downloadBookAsync(LibraryBook libraryBook, string tempAaxFilename)
|
||||
{
|
||||
validate(libraryBook);
|
||||
|
||||
var api = await GetApiAsync(libraryBook);
|
||||
|
||||
var actualFilePath = await PerformDownloadAsync(
|
||||
tempAaxFilename,
|
||||
(p) => api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, p));
|
||||
|
||||
System.Threading.Thread.Sleep(100);
|
||||
// if bad file download, a 0-33 byte file will be created
|
||||
// if service unavailable, a 52 byte string will be saved as file
|
||||
var length = new FileInfo(actualFilePath).Length;
|
||||
|
||||
if (length > 100)
|
||||
return actualFilePath;
|
||||
|
||||
var contents = File.ReadAllText(actualFilePath);
|
||||
File.Delete(actualFilePath);
|
||||
|
||||
var exMsg = contents.StartsWithInsensitive(SERVICE_UNAVAILABLE)
|
||||
? SERVICE_UNAVAILABLE
|
||||
: "Error downloading file";
|
||||
|
||||
var ex = new Exception(exMsg);
|
||||
Serilog.Log.Logger.Error(ex, "Download error {@DebugInfo}", new
|
||||
{
|
||||
libraryBook.Book.Title,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
libraryBook.Book.Locale,
|
||||
Account = libraryBook.Account?.ToMask() ?? "[empty]",
|
||||
tempAaxFilename,
|
||||
actualFilePath,
|
||||
length,
|
||||
contents
|
||||
});
|
||||
throw ex;
|
||||
}
|
||||
|
||||
private static void validate(LibraryBook libraryBook)
|
||||
{
|
||||
string errorString(string field)
|
||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||
|
||||
string errorTitle()
|
||||
{
|
||||
var title
|
||||
= (libraryBook.Book.Title.Length > 53)
|
||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
||||
: libraryBook.Book.Title;
|
||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||
return errorBookTitle;
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
||||
throw new Exception(errorString("Account"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
||||
throw new Exception(errorString("Locale"));
|
||||
}
|
||||
|
||||
private void moveBook(LibraryBook libraryBook, string actualFilePath)
|
||||
{
|
||||
var newAaxFilename = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.DownloadsFinal,
|
||||
libraryBook.Book.Title,
|
||||
"aax",
|
||||
libraryBook.Book.AudibleProductId);
|
||||
File.Move(actualFilePath, newAaxFilename);
|
||||
Invoke_StatusUpdate($"Successfully downloaded. Moved to: {newAaxFilename}");
|
||||
}
|
||||
|
||||
private static StatusHandler verifyDownload(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
|
||||
? new StatusHandler { "Downloaded AAX file cannot be found" }
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Dinah.Core.Net.Http;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
// frustratingly copy pasta from DownloadableBase and DownloadPdf
|
||||
public class DownloadFile : IDownloadable
|
||||
{
|
||||
public event EventHandler<string> DownloadBegin;
|
||||
public event EventHandler<DownloadProgress> DownloadProgressChanged;
|
||||
public event EventHandler<string> DownloadCompleted;
|
||||
|
||||
public async Task<string> PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
|
||||
var progress = new Progress<DownloadProgress>();
|
||||
progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e);
|
||||
|
||||
DownloadBegin?.Invoke(this, proposedDownloadFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||
return actualDownloadedFilePath;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DownloadCompleted?.Invoke(this, proposedDownloadFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadPdf : DownloadableBase
|
||||
{
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !AudibleFileStorage.PDF.Exists(libraryBook.Book.AudibleProductId);
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
return verifyDownload(libraryBook);
|
||||
}
|
||||
|
||||
private static StatusHandler verifyDownload(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.PDF.Exists(libraryBook.Book.AudibleProductId)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
// if audio file exists, get it's dir. else return base Book dir
|
||||
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
|
||||
var file = getdownloadUrl(libraryBook);
|
||||
|
||||
if (existingPath != null)
|
||||
return Path.Combine(existingPath, Path.GetFileName(file));
|
||||
|
||||
var full = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.PDF.StorageDirectory,
|
||||
libraryBook.Book.Title,
|
||||
Path.GetExtension(file),
|
||||
libraryBook.Book.AudibleProductId);
|
||||
return full;
|
||||
}
|
||||
|
||||
private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
{
|
||||
var api = await GetApiAsync(libraryBook);
|
||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
var client = new HttpClient();
|
||||
var actualDownloadedFilePath = await PerformDownloadAsync(
|
||||
proposedDownloadFilePath,
|
||||
(p) => client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, p));
|
||||
}
|
||||
|
||||
private static string getdownloadUrl(LibraryBook libraryBook)
|
||||
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public abstract class DownloadableBase : IDownloadableProcessable
|
||||
{
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public event EventHandler<string> DownloadBegin;
|
||||
public event EventHandler<DownloadProgress> DownloadProgressChanged;
|
||||
public event EventHandler<string> DownloadCompleted;
|
||||
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
|
||||
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
|
||||
public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessAsync()
|
||||
// often calls events which prints to forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
try
|
||||
{
|
||||
return await ProcessItemAsync(libraryBook);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
protected static Task<AudibleApi.Api> GetApiAsync(LibraryBook libraryBook)
|
||||
=> InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
|
||||
{
|
||||
var progress = new Progress<DownloadProgress>();
|
||||
progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e);
|
||||
|
||||
DownloadBegin?.Invoke(this, proposedDownloadFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await func(progress);
|
||||
StatusUpdate?.Invoke(this, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DownloadCompleted?.Invoke(this, proposedDownloadFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using AaxDecrypter;
|
||||
using AudibleApiDTOs;
|
||||
using Dinah.Core.Diagnostics;
|
||||
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadedChapters : Chapters
|
||||
{
|
||||
public DownloadedChapters(ChapterInfo chapterInfo)
|
||||
{
|
||||
AddChapters(chapterInfo.Chapters
|
||||
.Select(c => new AaxDecrypter.Chapter(c.StartOffsetMs / 1000d, (c.StartOffsetMs + c.LengthMs) / 1000d, c.Title)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public interface IDecryptable : IProcessable
|
||||
{
|
||||
event EventHandler<string> DecryptBegin;
|
||||
|
||||
event EventHandler<string> TitleDiscovered;
|
||||
event EventHandler<string> AuthorsDiscovered;
|
||||
event EventHandler<string> NarratorsDiscovered;
|
||||
event EventHandler<byte[]> CoverImageFilepathDiscovered;
|
||||
event EventHandler<int> UpdateProgress;
|
||||
|
||||
event EventHandler<string> DecryptCompleted;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using Dinah.Core.Net.Http;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public interface IDownloadable
|
||||
{
|
||||
event EventHandler<string> DownloadBegin;
|
||||
event EventHandler<DownloadProgress> DownloadProgressChanged;
|
||||
event EventHandler<string> DownloadCompleted;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace FileLiberator
|
||||
{
|
||||
public interface IDownloadableProcessable : IDownloadable, IProcessable { }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public interface IProcessable
|
||||
{
|
||||
event EventHandler<LibraryBook> Begin;
|
||||
|
||||
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
|
||||
event EventHandler<string> StatusUpdate;
|
||||
|
||||
event EventHandler<LibraryBook> Completed;
|
||||
|
||||
/// <returns>True == Valid</returns>
|
||||
bool Validate(LibraryBook libraryBook);
|
||||
|
||||
/// <returns>True == success</returns>
|
||||
Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public static class IProcessableExt
|
||||
{
|
||||
//
|
||||
// DO NOT USE ConfigureAwait(false) WITH ProcessAsync() unless ensuring ProcessAsync() implementation is cross-thread compatible
|
||||
// ProcessAsync() often does a lot with forms in the UI context
|
||||
//
|
||||
|
||||
|
||||
// when used in foreach: stateful. deferred execution
|
||||
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable)
|
||||
=> DbContexts.GetContext()
|
||||
.GetLibrary_Flat_NoTracking()
|
||||
.Where(libraryBook => processable.Validate(libraryBook));
|
||||
|
||||
public static LibraryBook GetSingleLibraryBook(string productId)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryBook = context
|
||||
.Library
|
||||
.GetLibrary()
|
||||
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
|
||||
return libraryBook;
|
||||
}
|
||||
|
||||
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook)
|
||||
{
|
||||
if (!processable.Validate(libraryBook))
|
||||
return new StatusHandler { "Validation failed" };
|
||||
|
||||
return await processable.ProcessBookAsync_NoValidation(libraryBook);
|
||||
}
|
||||
|
||||
public static async Task<StatusHandler> ProcessBookAsync_NoValidation(this IProcessable processable, LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(ProcessBookAsync_NoValidation) + " {@DebugInfo}", new
|
||||
{
|
||||
libraryBook.Book.Title,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
libraryBook.Book.Locale,
|
||||
Account = libraryBook.Account?.ToMask() ?? "[empty]"
|
||||
});
|
||||
|
||||
var status
|
||||
= (await processable.ProcessAsync(libraryBook))
|
||||
?? new StatusHandler { "Processable should never return a null status" };
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
|
||||
=> processable.Validate(libraryBook)
|
||||
? await processable.ProcessAsync(libraryBook)
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
// could add images here, but for now images are stored in a well-known location
|
||||
public enum FileType { Unknown, Audio, AAX, PDF }
|
||||
|
||||
/// <summary>
|
||||
/// Files are large. File contents are never read by app.
|
||||
/// Paths are varied.
|
||||
/// Files are written during download/decrypt/backup/liberate.
|
||||
/// Paths are read at app launch and during download/decrypt/backup/liberate.
|
||||
/// Many files are often looked up at once
|
||||
/// </summary>
|
||||
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
|
||||
{
|
||||
public abstract string[] Extensions { get; }
|
||||
public abstract string StorageDirectory { get; }
|
||||
|
||||
#region static
|
||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||
public static AudibleFileStorage AAX { get; } = new AaxFileStorage();
|
||||
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
|
||||
|
||||
public static string DownloadsInProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
|
||||
var AaxRootDir
|
||||
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
|
||||
return Directory.CreateDirectory(Path.Combine(AaxRootDir, "DownloadsInProgress")).FullName;
|
||||
}
|
||||
}
|
||||
|
||||
public static string DecryptInProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
|
||||
|
||||
var M4bRootDir
|
||||
= Configuration.Instance.DecryptInProgressEnum == "WinTemp"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
|
||||
return Directory.CreateDirectory(Path.Combine(M4bRootDir, "DecryptInProgress")).FullName;
|
||||
}
|
||||
}
|
||||
|
||||
// not customizable. don't move to config
|
||||
public static string DownloadsFinal => new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
|
||||
|
||||
public static string BooksDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
|
||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region instance
|
||||
public FileType FileType => (FileType)Value;
|
||||
|
||||
private IEnumerable<string> extensions_noDots { get; }
|
||||
private string extAggr { get; }
|
||||
|
||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
||||
{
|
||||
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
|
||||
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example for full books:
|
||||
/// Search recursively in _books directory. Full book exists if either are true
|
||||
/// - a directory name has the product id and an audio file is immediately inside
|
||||
/// - any audio filename contains the product id
|
||||
/// </summary>
|
||||
public bool Exists(string productId) => GetPath(productId) != null;
|
||||
|
||||
public string GetPath(string productId)
|
||||
{
|
||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
|
||||
var firstOrNull =
|
||||
Directory
|
||||
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => Regex.IsMatch(s, $@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase));
|
||||
|
||||
if (firstOrNull is null)
|
||||
return null;
|
||||
|
||||
FilePathCache.Upsert(productId, FileType, firstOrNull);
|
||||
return firstOrNull;
|
||||
}
|
||||
|
||||
public string GetDestDir(string title, string asin)
|
||||
{
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = title.IndexOf(':');
|
||||
var titleDir
|
||||
= underscoreIndex < 4
|
||||
? title
|
||||
: title.Substring(0, underscoreIndex);
|
||||
var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
|
||||
return finalDir;
|
||||
}
|
||||
|
||||
public bool IsFileTypeMatch(FileInfo fileInfo)
|
||||
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
public const string SKIP_FILE_EXT = "libhack";
|
||||
|
||||
public override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public AudioFileStorage() : base(FileType.Audio) { }
|
||||
|
||||
public string CreateSkipFile(string title, string asin, string contents = null)
|
||||
{
|
||||
var destinationDir = GetDestDir(title, asin);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var path = FileUtility.GetValidFilename(destinationDir, title, SKIP_FILE_EXT, asin);
|
||||
File.WriteAllText(path, contents ?? string.Empty);
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public class AaxFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "aax" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => DownloadsFinal;
|
||||
|
||||
public AaxFileStorage() : base(FileType.AAX) { }
|
||||
}
|
||||
|
||||
public class PdfFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "pdf", "zip" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public PdfFileStorage() : base(FileType.PDF) { }
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
// settings will be persisted when all are true
|
||||
// - property (not field)
|
||||
// - string
|
||||
// - public getter
|
||||
// - public setter
|
||||
|
||||
#region // properties to test reflection
|
||||
/*
|
||||
// field should NOT be populated
|
||||
public string TestField;
|
||||
// int should NOT be populated
|
||||
public int TestInt { get; set; }
|
||||
// read-only should NOT be populated
|
||||
public string TestGet { get; } // get only: should NOT get auto-populated
|
||||
// set-only should NOT be populated
|
||||
public string TestSet { private get; set; }
|
||||
|
||||
// get and set: SHOULD be auto-populated
|
||||
public string TestGetSet { get; set; }
|
||||
*/
|
||||
#endregion
|
||||
|
||||
private PersistentDictionary persistentDictionary;
|
||||
|
||||
public bool FilesExist
|
||||
=> File.Exists(APPSETTINGS_JSON)
|
||||
&& File.Exists(SettingsFilePath)
|
||||
&& Directory.Exists(LibationFiles)
|
||||
&& Directory.Exists(Books);
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public string Books
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(Books));
|
||||
set => persistentDictionary.Set(nameof(Books), value);
|
||||
}
|
||||
|
||||
private const string APP_DIR = "AppDir";
|
||||
public static string AppDir { get; } = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Exe.FileLocationOnDisk), LIBATION_FILES));
|
||||
public static string MyDocs { get; } = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), LIBATION_FILES));
|
||||
public static string WinTemp { get; } = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
|
||||
|
||||
private Dictionary<string, string> wellKnownPaths { get; } = new Dictionary<string, string>
|
||||
{
|
||||
[APP_DIR] = AppDir,
|
||||
["MyDocs"] = MyDocs,
|
||||
["WinTemp"] = WinTemp
|
||||
};
|
||||
private string libationFilesPathCache;
|
||||
|
||||
// default setting and directory creation occur in class responsible for files.
|
||||
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
|
||||
// exceptions: appsettings.json, LibationFiles dir, Settings.json
|
||||
|
||||
// temp/working dir(s) should be outside of dropbox
|
||||
[Description("Temporary location of files while they're in process of being downloaded.\r\nWhen download is complete, the final file will be in [LibationFiles]\\DownloadsFinal")]
|
||||
public string DownloadsInProgressEnum
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(DownloadsInProgressEnum));
|
||||
set => persistentDictionary.Set(nameof(DownloadsInProgressEnum), value);
|
||||
}
|
||||
|
||||
// temp/working dir(s) should be outside of dropbox
|
||||
[Description("Temporary location of files while they're in process of being decrypted.\r\nWhen decryption is complete, the final file will be in Books location")]
|
||||
public string DecryptInProgressEnum
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(DecryptInProgressEnum));
|
||||
set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value);
|
||||
}
|
||||
|
||||
[Description("Retain .aax files after decrypting?")]
|
||||
public bool RetainAaxFiles
|
||||
{
|
||||
get => persistentDictionary.Get<bool>(nameof(RetainAaxFiles));
|
||||
set => persistentDictionary.Set(nameof(RetainAaxFiles), value);
|
||||
}
|
||||
|
||||
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
|
||||
|
||||
// singleton stuff
|
||||
public static Configuration Instance { get; } = new Configuration();
|
||||
private Configuration() { }
|
||||
|
||||
private const string APPSETTINGS_JSON = "appsettings.json";
|
||||
private const string LIBATION_FILES = "LibationFiles";
|
||||
|
||||
[Description("Location for storage of program-created files")]
|
||||
public string LibationFiles => libationFilesPathCache ?? getLibationFiles();
|
||||
private string getLibationFiles()
|
||||
{
|
||||
var value = getLiberationFilesSettingFromJson();
|
||||
|
||||
// this looks weird but is correct for translating wellKnownPaths
|
||||
if (wellKnownPaths.ContainsKey(value))
|
||||
value = wellKnownPaths[value];
|
||||
|
||||
// must write here before SettingsFilePath in next step reads cache
|
||||
libationFilesPathCache = value;
|
||||
|
||||
// load json values into memory. create if not exists
|
||||
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
||||
|
||||
return libationFilesPathCache;
|
||||
}
|
||||
private string getLiberationFilesSettingFromJson()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(APPSETTINGS_JSON))
|
||||
{
|
||||
var appSettingsContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var jObj = JObject.Parse(appSettingsContents);
|
||||
|
||||
if (jObj.ContainsKey(LIBATION_FILES))
|
||||
{
|
||||
var value = jObj[LIBATION_FILES].Value<string>();
|
||||
|
||||
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
File.WriteAllText(APPSETTINGS_JSON, new JObject { { LIBATION_FILES, APP_DIR } }.ToString(Formatting.Indented));
|
||||
return APP_DIR;
|
||||
}
|
||||
|
||||
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName);
|
||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.Set(propertyName, newValue);
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue) => persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue);
|
||||
|
||||
public static string GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
.GetProperty(propertyName)
|
||||
?.GetCustomAttributes(typeof(DescriptionAttribute), true)
|
||||
.SingleOrDefault()
|
||||
as DescriptionAttribute;
|
||||
|
||||
return attribute?.Description;
|
||||
}
|
||||
|
||||
public bool TrySetLibationFiles(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory) && !wellKnownPaths.ContainsKey(directory))
|
||||
return false;
|
||||
|
||||
// if moving from default, delete old settings file and dir (if empty)
|
||||
if (LibationFiles.EqualsInsensitive(AppDir))
|
||||
{
|
||||
File.Delete(SettingsFilePath);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
if (!Directory.EnumerateDirectories(AppDir).Any() && !Directory.EnumerateFiles(AppDir).Any())
|
||||
Directory.Delete(AppDir);
|
||||
}
|
||||
|
||||
|
||||
libationFilesPathCache = null;
|
||||
|
||||
|
||||
var contents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var jObj = JObject.Parse(contents);
|
||||
|
||||
jObj[LIBATION_FILES] = directory;
|
||||
|
||||
var output = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
File.WriteAllText(APPSETTINGS_JSON, output);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.33" />
|
||||
<PackageReference Include="Octokit" Version="0.50.0" />
|
||||
<PackageReference Include="Polly" Version="7.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,94 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core.Collections.Immutable;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FilePathCache
|
||||
{
|
||||
internal class CacheEntry
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public FileType FileType { get; set; }
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
||||
|
||||
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
|
||||
|
||||
static FilePathCache()
|
||||
{
|
||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||
if (File.Exists(JsonFile))
|
||||
{
|
||||
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(JsonFile));
|
||||
cache = new Cache<CacheEntry>(list);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Exists(string id, FileType type) => GetPath(id, type) != null;
|
||||
|
||||
public static string GetPath(string id, FileType type)
|
||||
{
|
||||
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
||||
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
if (!File.Exists(entry.Path))
|
||||
{
|
||||
remove(entry);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.Path;
|
||||
}
|
||||
|
||||
private static void remove(CacheEntry entry)
|
||||
{
|
||||
cache.Remove(entry);
|
||||
save();
|
||||
}
|
||||
|
||||
public static void Upsert(string id, FileType type, string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException("Cannot add path to cache. File not found");
|
||||
|
||||
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
||||
|
||||
if (entry is null)
|
||||
cache.Add(new CacheEntry { Id = id, FileType = type, Path = path });
|
||||
else
|
||||
entry.Path = path;
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
// cache is thread-safe and lock free. but file saving is not
|
||||
private static object locker { get; } = new object();
|
||||
private static void save()
|
||||
{
|
||||
// create json if not exists
|
||||
static void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented));
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
try { resave(); }
|
||||
catch (IOException)
|
||||
{
|
||||
try { resave(); }
|
||||
catch (IOException ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FileUtility
|
||||
{
|
||||
public static string GetValidFilename(string dirFullPath, string filename, string extension, params string[] metadataSuffixes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dirFullPath))
|
||||
throw new ArgumentException($"{nameof(dirFullPath)} may not be null or whitespace", nameof(dirFullPath));
|
||||
|
||||
// file max length = 255. dir max len = 247
|
||||
|
||||
// sanitize
|
||||
filename = GetAsciiTag(filename);
|
||||
// manage length
|
||||
if (filename.Length > 50)
|
||||
filename = filename.Substring(0, 50) + "[...]";
|
||||
|
||||
// append id. it is 10 or 14 char in the common cases
|
||||
if (metadataSuffixes != null && metadataSuffixes.Length > 0)
|
||||
filename += " [" + string.Join("][", metadataSuffixes) + "]";
|
||||
|
||||
// this method may also be used for directory names, so no guarantee of extension
|
||||
if (!string.IsNullOrWhiteSpace(extension))
|
||||
extension = '.' + extension.Trim('.');
|
||||
|
||||
// ensure uniqueness
|
||||
var fullfilename = Path.Combine(dirFullPath, filename + extension);
|
||||
var i = 0;
|
||||
while (File.Exists(fullfilename))
|
||||
fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
|
||||
|
||||
return fullfilename;
|
||||
}
|
||||
|
||||
public static string GetAsciiTag(string property)
|
||||
{
|
||||
if (property == null)
|
||||
return "";
|
||||
|
||||
// omit characters which are invalid. EXCEPTION: change colon to underscore
|
||||
property = property.Replace(':', '_');
|
||||
|
||||
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
|
||||
foreach (var ch in Path.GetInvalidFileNameChars())
|
||||
property = property.Replace(ch.ToString(), "");
|
||||
return property;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public class PersistentDictionary
|
||||
{
|
||||
public string Filepath { get; }
|
||||
|
||||
// optimize for strings. expectation is most settings will be strings and a rare exception will be something else
|
||||
private Dictionary<string, string> stringCache { get; } = new Dictionary<string, string>();
|
||||
private Dictionary<string, object> objectCache { get; } = new Dictionary<string, object>();
|
||||
|
||||
public PersistentDictionary(string filepath)
|
||||
{
|
||||
Filepath = filepath;
|
||||
|
||||
if (File.Exists(Filepath))
|
||||
return;
|
||||
|
||||
// will create any missing directories, incl subdirectories. if all already exist: no action
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filepath));
|
||||
|
||||
File.WriteAllText(Filepath, "{}");
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
public string GetString(string propertyName)
|
||||
{
|
||||
if (!stringCache.ContainsKey(propertyName))
|
||||
{
|
||||
var jObject = readFile();
|
||||
stringCache[propertyName] = jObject.ContainsKey(propertyName) ? jObject[propertyName].Value<string>() : null;
|
||||
}
|
||||
|
||||
return stringCache[propertyName];
|
||||
}
|
||||
|
||||
public T Get<T>(string propertyName)
|
||||
{
|
||||
var o = GetObject(propertyName);
|
||||
if (o is null) return default;
|
||||
if (o is JToken jt) return jt.Value<T>();
|
||||
return (T)o;
|
||||
}
|
||||
|
||||
public object GetObject(string propertyName)
|
||||
{
|
||||
if (!objectCache.ContainsKey(propertyName))
|
||||
{
|
||||
var jObject = readFile();
|
||||
objectCache[propertyName] = jObject.ContainsKey(propertyName) ? jObject[propertyName].Value<object>() : null;
|
||||
}
|
||||
|
||||
return objectCache[propertyName];
|
||||
}
|
||||
|
||||
private object locker { get; } = new object();
|
||||
public void Set(string propertyName, string newValue)
|
||||
{
|
||||
// only do this check in string cache, NOT object cache
|
||||
if (stringCache[propertyName] == newValue)
|
||||
return;
|
||||
|
||||
// set cache
|
||||
stringCache[propertyName] = newValue;
|
||||
|
||||
writeFile(propertyName, newValue);
|
||||
}
|
||||
|
||||
public void Set(string propertyName, object newValue)
|
||||
{
|
||||
// set cache
|
||||
objectCache[propertyName] = newValue;
|
||||
|
||||
var parsedNewValue = JToken.Parse(JsonConvert.SerializeObject(newValue));
|
||||
writeFile(propertyName, parsedNewValue);
|
||||
}
|
||||
|
||||
private void writeFile(string propertyName, JToken newValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var str = newValue?.ToString();
|
||||
var formattedValue
|
||||
= str is null ? "[null]"
|
||||
: string.IsNullOrEmpty(str) ? "[empty]"
|
||||
: string.IsNullOrWhiteSpace(str) ? $"[whitespace. Length={str.Length}]"
|
||||
: str.Length > 100 ? $"[Length={str.Length}] {str[0..50]}...{str[^50..^0]}"
|
||||
: str;
|
||||
Serilog.Log.Logger.Information($"Config changed. {propertyName}={formattedValue}");
|
||||
}
|
||||
catch { }
|
||||
|
||||
// write new setting to file
|
||||
lock (locker)
|
||||
{
|
||||
var jObject = readFile();
|
||||
jObject[propertyName] = newValue;
|
||||
File.WriteAllText(Filepath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
|
||||
}
|
||||
}
|
||||
|
||||
// special case: no caching. no logging
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var jObject = readFile();
|
||||
var token = jObject.SelectToken(jsonPath);
|
||||
var debug_oldValue = (string)token[propertyName];
|
||||
|
||||
token[propertyName] = newValue;
|
||||
File.WriteAllText(Filepath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
|
||||
}
|
||||
}
|
||||
|
||||
private JObject readFile()
|
||||
{
|
||||
var settingsJsonContents = File.ReadAllText(Filepath);
|
||||
var jObject = JsonConvert.DeserializeObject<JObject>(settingsJsonContents);
|
||||
return jObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public enum PictureSize { _80x80, _300x300, _500x500 }
|
||||
public struct PictureDefinition
|
||||
{
|
||||
public string PictureId { get; }
|
||||
public PictureSize Size { get; }
|
||||
|
||||
public PictureDefinition(string pictureId, PictureSize pictureSize)
|
||||
{
|
||||
PictureId = pictureId;
|
||||
Size = pictureSize;
|
||||
}
|
||||
}
|
||||
public static class PictureStorage
|
||||
{
|
||||
// not customizable. don't move to config
|
||||
private static string ImagesDirectory { get; }
|
||||
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("Images").FullName;
|
||||
|
||||
private static string getPath(PictureDefinition def)
|
||||
=> Path.Combine(ImagesDirectory, $"{def.PictureId}{def.Size}.jpg");
|
||||
|
||||
private static System.Timers.Timer timer { get; }
|
||||
static PictureStorage()
|
||||
{
|
||||
timer = new System.Timers.Timer(700)
|
||||
{
|
||||
AutoReset = true,
|
||||
Enabled = true
|
||||
};
|
||||
timer.Elapsed += (_, __) => timerDownload();
|
||||
}
|
||||
|
||||
public static event EventHandler<string> PictureCached;
|
||||
|
||||
private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>();
|
||||
public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def)
|
||||
{
|
||||
if (!cache.ContainsKey(def))
|
||||
{
|
||||
var path = getPath(def);
|
||||
cache[def]
|
||||
= File.Exists(path)
|
||||
? File.ReadAllBytes(path)
|
||||
: null;
|
||||
}
|
||||
return (cache[def] == null, cache[def] ?? getDefaultImage(def.Size));
|
||||
}
|
||||
|
||||
private static Dictionary<PictureSize, byte[]> defaultImages { get; } = new Dictionary<PictureSize, byte[]>();
|
||||
public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes)
|
||||
=> defaultImages[pictureSize] = bytes;
|
||||
private static byte[] getDefaultImage(PictureSize size)
|
||||
=> defaultImages.ContainsKey(size)
|
||||
? defaultImages[size]
|
||||
: new byte[0];
|
||||
|
||||
// necessary to avoid IO errors. ReadAllBytes and WriteAllBytes can conflict in some cases, esp when debugging
|
||||
private static bool isProcessing;
|
||||
private static void timerDownload()
|
||||
{
|
||||
// must live outside try-catch, else 'finally' can reset another thread's lock
|
||||
if (isProcessing)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
isProcessing = true;
|
||||
|
||||
var def = cache
|
||||
.Where(kvp => kvp.Value is null)
|
||||
.Select(kvp => kvp.Key)
|
||||
// 80x80 should be 1st since it's enum value == 0
|
||||
.OrderBy(d => d.PictureId)
|
||||
.FirstOrDefault();
|
||||
|
||||
// no more null entries. all requsted images are cached
|
||||
if (string.IsNullOrWhiteSpace(def.PictureId))
|
||||
return;
|
||||
|
||||
var bytes = downloadBytes(def);
|
||||
saveFile(def, bytes);
|
||||
cache[def] = bytes;
|
||||
|
||||
PictureCached?.Invoke(nameof(PictureStorage), def.PictureId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient imageDownloadClient { get; } = new HttpClient();
|
||||
private static byte[] downloadBytes(PictureDefinition def)
|
||||
{
|
||||
var sz = def.Size.ToString().Split('x')[1];
|
||||
return imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}._SL{sz}_.jpg").Result;
|
||||
}
|
||||
|
||||
private static void saveFile(PictureDefinition def, byte[] bytes)
|
||||
{
|
||||
var path = getPath(def);
|
||||
File.WriteAllBytes(path, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApiDTOs;
|
||||
using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace InternalUtilities
|
||||
{
|
||||
public static class AudibleApiActions
|
||||
{
|
||||
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
|
||||
public static Task<Api> GetApiAsync(string username, string localeName, ILoginCallback loginCallback = null)
|
||||
{
|
||||
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
|
||||
{
|
||||
Username = username.ToMask(),
|
||||
LocaleName = localeName,
|
||||
});
|
||||
return EzApiCreator.GetApiAsync(
|
||||
Localization.Get(localeName),
|
||||
AudibleApiStorage.AccountsSettingsFile,
|
||||
AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName),
|
||||
loginCallback);
|
||||
}
|
||||
|
||||
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
|
||||
public static Task<Api> GetApiAsync(ILoginCallback loginCallback, Account account)
|
||||
{
|
||||
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
|
||||
{
|
||||
Account = account?.MaskedLogEntry ?? "[null]",
|
||||
LocaleName = account?.Locale?.Name
|
||||
});
|
||||
return EzApiCreator.GetApiAsync(
|
||||
account.Locale,
|
||||
AudibleApiStorage.AccountsSettingsFile,
|
||||
account.GetIdentityTokensJsonPath(),
|
||||
loginCallback);
|
||||
}
|
||||
|
||||
private static AsyncRetryPolicy policy { get; }
|
||||
= Policy.Handle<Exception>()
|
||||
// 2 retries == 3 total
|
||||
.RetryAsync(2);
|
||||
|
||||
public static Task<List<Item>> GetLibraryValidatedAsync(Api api)
|
||||
{
|
||||
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or tokens are refreshed
|
||||
// worse, this 1st dummy call doesn't seem to help:
|
||||
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
|
||||
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
|
||||
return policy.ExecuteAsync(() => getItemsAsync(api));
|
||||
}
|
||||
|
||||
private static async Task<List<Item>> getItemsAsync(Api api)
|
||||
{
|
||||
var items = await api.GetAllLibraryItemsAsync();
|
||||
|
||||
// remove episode parents and 'audible plus' check-outs
|
||||
items.RemoveAll(i => i.IsEpisodes || i.IsNonLibraryAudiblePlus);
|
||||
|
||||
#region // episode handling. doesn't quite work
|
||||
// // add individual/children episodes
|
||||
// var childIds = items
|
||||
// .Where(i => i.Episodes)
|
||||
// .SelectMany(ep => ep.Relationships)
|
||||
// .Where(r => r.RelationshipToProduct == AudibleApiDTOs.RelationshipToProduct.Child && r.RelationshipType == AudibleApiDTOs.RelationshipType.Episode)
|
||||
// .Select(c => c.Asin)
|
||||
// .ToList();
|
||||
// foreach (var childId in childIds)
|
||||
// {
|
||||
// var bookResult = await api.GetLibraryBookAsync(childId, AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
// var bookItem = AudibleApiDTOs.LibraryDtoV10.FromJson(bookResult.ToString()).Item;
|
||||
// items.Add(bookItem);
|
||||
// }
|
||||
#endregion
|
||||
|
||||
var validators = new List<IValidator>();
|
||||
validators.AddRange(getValidators());
|
||||
foreach (var v in validators)
|
||||
{
|
||||
var exceptions = v.Validate(items);
|
||||
if (exceptions != null && exceptions.Any())
|
||||
throw new AggregateException(exceptions);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static List<IValidator> getValidators()
|
||||
{
|
||||
var type = typeof(IValidator);
|
||||
var types = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(s => s.GetTypes())
|
||||
.Where(p => type.IsAssignableFrom(p) && !p.IsInterface);
|
||||
|
||||
return types.Select(t => Activator.CreateInstance(t) as IValidator).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
263
Libation.sln
@@ -1,263 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.28803.156
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
__TODO.txt = __TODO.txt
|
||||
_DB_NOTES.txt = _DB_NOTES.txt
|
||||
REFERENCE.txt = REFERENCE.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5 Domain Utilities (db aware)", "5 Domain Utilities (db aware)", "{41CDCC73-9B81-49DD-9570-C54406E852AF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "6 Application", "6 Application", "{8679CAC8-9164-4007-BDD2-F004810EDA14}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1 Core Libraries", "1 Core Libraries", "{43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 Domain (db)", "4 Domain (db)", "{751093DD-5DBA-463E-ADBE-E05FAFB6983E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2 Utilities (domain ignorant)", "2 Utilities (domain ignorant)", "{7FBBB086-0807-4998-85BF-6D1A49C8AD05}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AaxDecrypter", "AaxDecrypter\AaxDecrypter.csproj", "{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager", "FileManager\FileManager.csproj", "{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataLayer", "DataLayer\DataLayer.csproj", "{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator", "FileLiberator\FileLiberator.csproj", "{393B5B27-D15C-4F77-9457-FA14BA8F3C73}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities", "InternalUtilities\InternalUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities (db ignorant)", "3 Domain Internal Utilities (db ignorant)", "{F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "LibationSearchEngine\LibationSearchEngine.csproj", "{2E1F5DB4-40CC-4804-A893-5DCE0193E598}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Tests", "0 Tests", "{38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Demos and Examples", "0 Demos and Examples", "{F61184E7-2426-4A13-ACEF-5689928E2CE2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Demos", "..\Dinah.Core\_Demos\Dinah.Core.Demos\Dinah.Core.Demos.csproj", "{9F1AA3DE-962F-469B-82B2-46F93491389B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Tests", "..\Dinah.Core\_Tests\Dinah.Core.Tests\Dinah.Core.Tests.csproj", "{E874D000-AD3A-4629-AC65-7219C2C7C1F0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCommon", "..\Dinah.Core\_Tests\TestCommon\TestCommon.csproj", "{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitAllRepos", "..\GitAllRepos\GitAllRepos\GitAllRepos.csproj", "{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi", "..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj", "{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi.Tests", "..\audible api\AudibleApi\_Tests\AudibleApi.Tests\AudibleApi.Tests.csproj", "{111420E2-D4F0-4068-B46A-C4B6DCC823DC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "LibationWinForms\LibationWinForms.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsDesigner", "WinFormsDesigner\WinFormsDesigner.csproj", "{0807616A-A77A-4B08-A65A-1582B09E114B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ffmpeg decrypt", "_Demos\ffmpeg decrypt\ffmpeg decrypt.csproj", "{DF72740C-900A-45DA-A3A6-4DDD68F286F2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "inAudibleLite", "_Demos\inAudibleLite\inAudibleLite.csproj", "{74D02251-898E-4CAF-80C7-801820622903}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core", "..\Dinah.Core\Dinah.Core\Dinah.Core.csproj", "{9E951521-2587-4FC6-AD26-FAA9179FB6C4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore", "..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj", "{1255D9BA-CE6E-42E4-A253-6376540B9661}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2", "..\LuceneNet303r2\LuceneNet303r2\LuceneNet303r2.csproj", "{35803735-B669-4090-9681-CC7F7FABDC71}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2.Tests", "..\LuceneNet303r2\LuceneNet303r2.Tests\LuceneNet303r2.Tests.csproj", "{5A7681A5-60D9-480B-9AC7-63E0812A2548}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoImporterService\DtoImporterService.csproj", "{401865F5-1942-4713-B230-04544C0A97B0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs", "..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj", "{C03C5D65-3B7F-453B-972F-23950B7E0604}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs.Tests", "..\audible api\AudibleApi\_Tests\AudibleApiDTOs.Tests\AudibleApiDTOs.Tests.csproj", "{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiClientExample", "..\audible api\AudibleApi\_Demos\AudibleApiClientExample\AudibleApiClientExample.csproj", "{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop", "..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj", "{059CE32C-9AD6-45E9-A166-790DFFB0B730}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsDesktopUtilities", "WindowsDesktopUtilities\WindowsDesktopUtilities.csproj", "{E7EFD64D-6630-4426-B09C-B6862A92E3FD}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libation Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests", "_Tests\LibationSearchEngine.Tests\LibationSearchEngine.Tests.csproj", "{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.Tests", "..\Dinah.Core\_Tests\Dinah.EntityFrameworkCore.Tests\Dinah.EntityFrameworkCore.Tests.csproj", "{6F5131A0-09AE-4707-B82B-5E53CB74688E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{74D02251-898E-4CAF-80C7-801820622903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{74D02251-898E-4CAF-80C7-801820622903}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{74D02251-898E-4CAF-80C7-801820622903}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{74D02251-898E-4CAF-80C7-801820622903}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E}
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{74D02251-898E-4CAF-80C7-801820622903} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||
<AssemblyName>Libation</AssemblyName>
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<Version>4.4.0.5</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSBump" Version="2.3.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Octokit" Version="0.50.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LibationWinForms\LibationWinForms.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,428 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms;
|
||||
using LibationWinForms.Dialogs;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
namespace LibationLauncher
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
createSettings();
|
||||
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
migrate_to_v4_0_0();
|
||||
migrate_to_v4_0_3(); // add setting for whether to delete/retain aax
|
||||
|
||||
ensureLoggingConfig();
|
||||
ensureSerilogConfig();
|
||||
configureLogging();
|
||||
checkForUpdate();
|
||||
logStartupState();
|
||||
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
|
||||
private static void createSettings()
|
||||
{
|
||||
static bool configSetupIsComplete(Configuration config)
|
||||
=> config.FilesExist
|
||||
&& !string.IsNullOrWhiteSpace(config.DownloadsInProgressEnum)
|
||||
&& !string.IsNullOrWhiteSpace(config.DecryptInProgressEnum);
|
||||
|
||||
var config = Configuration.Instance;
|
||||
if (configSetupIsComplete(config))
|
||||
return;
|
||||
|
||||
var isAdvanced = false;
|
||||
|
||||
var setupDialog = new SetupDialog();
|
||||
setupDialog.NoQuestionsBtn_Click += (_, __) =>
|
||||
{
|
||||
config.DownloadsInProgressEnum ??= "WinTemp";
|
||||
config.DecryptInProgressEnum ??= "WinTemp";
|
||||
config.Books ??= Configuration.AppDir;
|
||||
};
|
||||
// setupDialog.BasicBtn_Click += (_, __) => // no action needed
|
||||
setupDialog.AdvancedBtn_Click += (_, __) => isAdvanced = true;
|
||||
setupDialog.ShowDialog();
|
||||
|
||||
if (isAdvanced)
|
||||
{
|
||||
var dialog = new LibationFilesDialog();
|
||||
if (dialog.ShowDialog() != DialogResult.OK)
|
||||
MessageBox.Show("Libation Files location not changed");
|
||||
}
|
||||
|
||||
if (configSetupIsComplete(config))
|
||||
return;
|
||||
|
||||
if (new SettingsDialog().ShowDialog() == DialogResult.OK)
|
||||
return;
|
||||
|
||||
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
Application.Exit();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
#region v3 => v4 migration
|
||||
static string AccountsSettingsFileLegacy30 => Path.Combine(Configuration.Instance.LibationFiles, "IdentityTokens.json");
|
||||
|
||||
private static void migrate_to_v4_0_0()
|
||||
{
|
||||
migrateLegacyIdentityFile();
|
||||
|
||||
updateSettingsFile();
|
||||
}
|
||||
|
||||
private static void migrateLegacyIdentityFile()
|
||||
{
|
||||
if (File.Exists(AccountsSettingsFileLegacy30))
|
||||
{
|
||||
// don't always rely on applicable POCOs. some is legacy and must be: json file => JObject
|
||||
try
|
||||
{
|
||||
updateLegacyFileWithLocale();
|
||||
|
||||
var account = createAccountFromLegacySettings();
|
||||
account.DecryptKey = getDecryptKey(account);
|
||||
|
||||
// the next few methods need persistence. to be a good citizen, dispose of persister at the end of current scope
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
persister.AccountsSettings.Add(account);
|
||||
}
|
||||
// migration is a convenience. if something goes wrong: just move on
|
||||
catch { }
|
||||
|
||||
// delete legacy token file
|
||||
File.Delete(AccountsSettingsFileLegacy30);
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateLegacyFileWithLocale()
|
||||
{
|
||||
var legacyContents = File.ReadAllText(AccountsSettingsFileLegacy30);
|
||||
var legacyJObj = JObject.Parse(legacyContents);
|
||||
|
||||
// attempt to update legacy token file with locale from settings
|
||||
if (!legacyJObj.ContainsKey("LocaleName"))
|
||||
{
|
||||
var settings = File.ReadAllText(Configuration.Instance.SettingsFilePath);
|
||||
var settingsJObj = JObject.Parse(settings);
|
||||
if (settingsJObj.TryGetValue("LocaleCountryCode", out var localeName))
|
||||
{
|
||||
// update legacy token file with locale from settings
|
||||
legacyJObj.AddFirst(new JProperty("LocaleName", localeName.Value<string>()));
|
||||
|
||||
// save
|
||||
var newContents = legacyJObj.ToString(Formatting.Indented);
|
||||
File.WriteAllText(AccountsSettingsFileLegacy30, newContents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Account createAccountFromLegacySettings()
|
||||
{
|
||||
// get required locale from settings file
|
||||
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
|
||||
if (!JObject.Parse(settingsContents).TryGetValue("LocaleCountryCode", out var jLocale))
|
||||
return null;
|
||||
|
||||
var localeName = jLocale.Value<string>();
|
||||
var locale = Localization.Get(localeName);
|
||||
|
||||
var api = EzApiCreator.GetApiAsync(locale, AccountsSettingsFileLegacy30).GetAwaiter().GetResult();
|
||||
var email = api.GetEmailAsync().GetAwaiter().GetResult();
|
||||
|
||||
// identity has likely been updated above. re-get contents
|
||||
var legacyContents = File.ReadAllText(AccountsSettingsFileLegacy30);
|
||||
|
||||
var identity = AudibleApi.Authorization.Identity.FromJson(legacyContents);
|
||||
|
||||
if (!identity.IsValid)
|
||||
return null;
|
||||
|
||||
var account = new Account(email)
|
||||
{
|
||||
AccountName = $"{email} - {locale.Name}",
|
||||
LibraryScan = true,
|
||||
IdentityTokens = identity
|
||||
};
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
private static string getDecryptKey(Account account)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(account?.DecryptKey))
|
||||
return account.DecryptKey;
|
||||
|
||||
if (!File.Exists(Configuration.Instance.SettingsFilePath) || account is null)
|
||||
return "";
|
||||
|
||||
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
|
||||
if (JObject.Parse(settingsContents).TryGetValue("DecryptKey", out var jToken))
|
||||
return jToken.Value<string>() ?? "";
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static void updateSettingsFile()
|
||||
{
|
||||
if (!File.Exists(Configuration.Instance.SettingsFilePath))
|
||||
return;
|
||||
|
||||
// use JObject to remove decrypt key and locale from Settings.json
|
||||
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
|
||||
var jObj = JObject.Parse(settingsContents);
|
||||
|
||||
var jLocale = jObj.Property("LocaleCountryCode");
|
||||
var jDecryptKey = jObj.Property("DecryptKey");
|
||||
|
||||
jDecryptKey?.Remove();
|
||||
jLocale?.Remove();
|
||||
|
||||
if (jDecryptKey != null || jLocale != null)
|
||||
{
|
||||
var newContents = jObj.ToString(Formatting.Indented);
|
||||
File.WriteAllText(Configuration.Instance.SettingsFilePath, newContents);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region migrate_to_v4_0_3 add setting for whether to delete/retain aax
|
||||
private static void migrate_to_v4_0_3()
|
||||
{
|
||||
if (!File.Exists(Configuration.Instance.SettingsFilePath))
|
||||
return;
|
||||
|
||||
// use JObject to remove decrypt key and locale from Settings.json
|
||||
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
|
||||
var jObj = JObject.Parse(settingsContents);
|
||||
|
||||
var jRetainAaxFiles = jObj.Property("RetainAaxFiles");
|
||||
if (jRetainAaxFiles is null)
|
||||
{
|
||||
jObj.Add("RetainAaxFiles", false);
|
||||
|
||||
var newContents = jObj.ToString(Formatting.Indented);
|
||||
File.WriteAllText(Configuration.Instance.SettingsFilePath, newContents);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static string defaultLoggingLevel { get; } = "Information";
|
||||
private static void ensureLoggingConfig()
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
if (config.GetObject("Logging") != null)
|
||||
return;
|
||||
|
||||
// "Logging": {
|
||||
// "LogLevel": {
|
||||
// "Default": "Debug"
|
||||
// }
|
||||
// }
|
||||
var loggingObj = new JObject
|
||||
{
|
||||
{
|
||||
"LogLevel", new JObject { { "Default", defaultLoggingLevel } }
|
||||
}
|
||||
};
|
||||
config.SetObject("Logging", loggingObj);
|
||||
}
|
||||
|
||||
private static void ensureSerilogConfig()
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
if (config.GetObject("Serilog") != null)
|
||||
return;
|
||||
|
||||
// default. for reference. output example:
|
||||
// 2019-11-26 08:48:40.224 -05:00 [DBG] Begin Libation
|
||||
var default_outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}";
|
||||
// with class and method info. output example:
|
||||
// 2019-11-26 08:48:40.224 -05:00 [DBG] (at LibationWinForms.Program.init()) Begin Libation
|
||||
var code_outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}";
|
||||
|
||||
// "Serilog": {
|
||||
// "MinimumLevel": "Information"
|
||||
// "WriteTo": [
|
||||
// {
|
||||
// "Name": "Console"
|
||||
// },
|
||||
// {
|
||||
// "Name": "File",
|
||||
// "Args": {
|
||||
// "rollingInterval": "Day",
|
||||
// "outputTemplate": ...
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
// "Using": [ "Dinah.Core" ],
|
||||
// "Enrich": [ "WithCaller" ]
|
||||
// }
|
||||
var serilogObj = new JObject
|
||||
{
|
||||
{ "MinimumLevel", defaultLoggingLevel },
|
||||
{ "WriteTo", new JArray
|
||||
{
|
||||
new JObject { {"Name", "Console" } },
|
||||
new JObject
|
||||
{
|
||||
{ "Name", "File" },
|
||||
{ "Args",
|
||||
new JObject
|
||||
{
|
||||
// for this sink to work, a path must be provided. we override this below
|
||||
{ "path", Path.Combine(Configuration.Instance.LibationFiles, "_Log.log") },
|
||||
{ "rollingInterval", "Month" },
|
||||
{ "outputTemplate", code_outputTemplate }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "Using", new JArray{ "Dinah.Core" } }, // dll's name, NOT namespace
|
||||
{ "Enrich", new JArray{ "WithCaller" } },
|
||||
};
|
||||
config.SetObject("Serilog", serilogObj);
|
||||
}
|
||||
|
||||
private static void configureLogging()
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
// override path. always use current libation files
|
||||
var logPath = Path.Combine(Configuration.Instance.LibationFiles, "Log.log");
|
||||
config.SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath);
|
||||
|
||||
//// hack which achieves the same
|
||||
//configuration["Serilog:WriteTo:1:Args:path"] = logPath;
|
||||
|
||||
// CONFIGURATION-DRIVEN (json)
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(config.SettingsFilePath)
|
||||
.Build();
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
|
||||
//// MANUAL HARD CODED
|
||||
//Log.Logger = new LoggerConfiguration()
|
||||
// // requires: using Dinah.Core.Logging;
|
||||
// .Enrich.WithCaller()
|
||||
// .MinimumLevel.Information()
|
||||
// .WriteTo.File(logPath,
|
||||
// rollingInterval: RollingInterval.Month,
|
||||
// outputTemplate: code_outputTemplate)
|
||||
// .CreateLogger();
|
||||
|
||||
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
|
||||
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
|
||||
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
|
||||
}
|
||||
|
||||
private static void checkForUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue("Libation"));
|
||||
|
||||
// https://octokitnet.readthedocs.io/en/latest/releases/
|
||||
var releases = gitHubClient.Repository.Release.GetAll("rmcrackan", "Libation").GetAwaiter().GetResult();
|
||||
var latest = releases.First(r => !r.Draft);
|
||||
|
||||
var latestVersionString = latest.TagName.Trim('v');
|
||||
if (!Version.TryParse(latestVersionString, out var latestRelease))
|
||||
return;
|
||||
|
||||
// we're up to date
|
||||
if (latestRelease <= BuildVersion)
|
||||
return;
|
||||
|
||||
// we have an update
|
||||
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
|
||||
var zipUrl = zip?.BrowserDownloadUrl;
|
||||
if (zipUrl is null)
|
||||
{
|
||||
MessageBox.Show(latest.HtmlUrl, "New version available");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show($"New version available @ {latest.HtmlUrl}\r\nDownload the zip file?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
|
||||
if (result != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
using var fileSelector = new SaveFileDialog { FileName = zip.Name, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
|
||||
if (fileSelector.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
var selectedPath = fileSelector.FileName;
|
||||
|
||||
try
|
||||
{
|
||||
LibationWinForms.BookLiberation.ProcessorAutomationController.DownloadFileAsync(zipUrl, selectedPath).GetAwaiter().GetResult();
|
||||
MessageBox.Show("File downloaded");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error(ex, "Error downloading update");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error(ex, "Error checking for update");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Error(Exception ex, string message)
|
||||
{
|
||||
Log.Logger.Error(ex, message);
|
||||
MessageBox.Show($"{message}\r\nSee log for details");
|
||||
}
|
||||
|
||||
private static void logStartupState()
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
Log.Logger.Information("Begin Libation. {@DebugInfo}", new
|
||||
{
|
||||
Version = BuildVersion.ToString(),
|
||||
|
||||
config.LibationFiles,
|
||||
AudibleFileStorage.BooksDirectory,
|
||||
|
||||
config.DownloadsInProgressEnum,
|
||||
DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
|
||||
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
|
||||
|
||||
AudibleFileStorage.DownloadsFinal,
|
||||
DownloadsFinalFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsFinal).Count(),
|
||||
|
||||
config.DecryptInProgressEnum,
|
||||
DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
|
||||
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
|
||||
});
|
||||
}
|
||||
|
||||
private static Version BuildVersion => System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
}
|
||||
}
|
||||