Compare commits
846 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4001124cfa | ||
|
|
f86bdba3c3 | ||
|
|
98c3940297 | ||
|
|
b9e789bbcf | ||
|
|
a108846731 | ||
|
|
0b4ce8d6e7 | ||
|
|
42df61b7dd | ||
|
|
6b46fa4cbc | ||
|
|
c0762eba18 | ||
|
|
036fb848e1 | ||
|
|
7198ae9025 | ||
|
|
d2822b06aa | ||
|
|
17feca28b9 | ||
|
|
898d38cb6a | ||
|
|
95a99a2f0b | ||
|
|
29a1e8ad34 | ||
|
|
19f3a4f266 | ||
|
|
12ddbc308a | ||
|
|
999bc7604e | ||
|
|
3648de3a8d | ||
|
|
051fa0a28f | ||
|
|
72e667e825 | ||
|
|
5ed59b41b5 | ||
|
|
c7c0d1632e | ||
|
|
2dc73acd20 | ||
|
|
ed71668c48 | ||
|
|
801e154d15 | ||
|
|
a89b07394f | ||
|
|
982f9b7c58 | ||
|
|
789b9207b5 | ||
|
|
133dbb7471 | ||
|
|
5d3ec493cd | ||
|
|
6d7f234497 | ||
|
|
29a50bb640 | ||
|
|
843fddabde | ||
|
|
109ce0dd1f | ||
|
|
42508a82a0 | ||
|
|
d860d39f5f | ||
|
|
15396c611a | ||
|
|
41c4b12ae1 | ||
|
|
e51c30462f | ||
|
|
9b5df99a61 | ||
|
|
3535156ea5 | ||
|
|
577145096d | ||
|
|
89059510fd | ||
|
|
aabc14c639 | ||
|
|
c28872544c | ||
|
|
7b8a4e4d72 | ||
|
|
5dcdf670be | ||
|
|
9721890a3c | ||
|
|
1b9c4cfc23 | ||
|
|
98a552e9af | ||
|
|
e1e265a101 | ||
|
|
b60a854de0 | ||
|
|
d1bddeccc8 | ||
|
|
0a106e64d8 | ||
|
|
91d6181aec | ||
|
|
255c0a3359 | ||
|
|
3a5ef999f0 | ||
|
|
983aa845d6 | ||
|
|
d1779726e6 | ||
|
|
8e23062d0e | ||
|
|
7efbfffd99 | ||
|
|
ff4b2d2ecc | ||
|
|
e079be0ad7 | ||
|
|
a8a54aa443 | ||
|
|
88cbcf6baf | ||
|
|
8d6d26c9d2 | ||
|
|
a490df0f7e | ||
|
|
a46041c958 | ||
|
|
0a6a78bc58 | ||
|
|
c9e850515e | ||
|
|
0ff8da2cf0 | ||
|
|
c0ef3ccbea | ||
|
|
1ab628dee8 | ||
|
|
b24df24b10 | ||
|
|
341678d979 | ||
|
|
49d10273a6 | ||
|
|
5b05c018d5 | ||
|
|
d18d8c0ba4 | ||
|
|
84a8fb0074 | ||
|
|
a40fb7f4bd | ||
|
|
84eb3a3508 | ||
|
|
73a5d76503 | ||
|
|
50c35ed519 | ||
|
|
a7b7e3efea | ||
|
|
88e892196f | ||
|
|
7f08da96bb | ||
|
|
193f24768e | ||
|
|
a8bca3de98 | ||
|
|
9692a802d0 | ||
|
|
28a8b2e685 | ||
|
|
3c9121b4af | ||
|
|
dec1035258 | ||
|
|
9d81c86c1b | ||
|
|
eeb4f4681a | ||
|
|
676af0210b | ||
|
|
77c6a2890b | ||
|
|
c39e748749 | ||
|
|
36e5a6ac8d | ||
|
|
9bdcaa5eaa | ||
|
|
5511004db8 | ||
|
|
0e46cdb514 | ||
|
|
b028899949 | ||
|
|
55285427f1 | ||
|
|
763a6cb31a | ||
|
|
24cb1aa84f | ||
|
|
886aa4938d | ||
|
|
8871651549 | ||
|
|
2ae8ef87d9 | ||
|
|
de4fbe05f7 | ||
|
|
b8abed37c2 | ||
|
|
255e26435c | ||
|
|
9e0550619b | ||
|
|
5c171fd0f0 | ||
|
|
3dd3b710b7 | ||
|
|
bce3bdba7e | ||
|
|
360f077da3 | ||
|
|
75c5f662dc | ||
|
|
3c0485cfa9 | ||
|
|
d5ba405de0 | ||
|
|
71b8bca86d | ||
|
|
c53b9eabd6 | ||
|
|
6d6434b4d4 | ||
|
|
a447e88b86 | ||
|
|
e2d2e00913 | ||
|
|
cbfea37b3a | ||
|
|
d6de647974 | ||
|
|
b784bd6b8d | ||
|
|
00df6da366 | ||
|
|
dad36c73e5 | ||
|
|
936a1d60a0 | ||
|
|
e0248c2d8e | ||
|
|
b12731e3d5 | ||
|
|
9636aca47c | ||
|
|
4138183352 | ||
|
|
c3871d3bca | ||
|
|
dd8b0783a9 | ||
|
|
9a50aa4c7c | ||
|
|
c40185030f | ||
|
|
7cba28019c | ||
|
|
926f8a957e | ||
|
|
59aeaf24e4 | ||
|
|
64eaa157e5 | ||
|
|
9a5d9f3867 | ||
|
|
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 | ||
|
|
ff20d777a6 |
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,123 +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>
|
||||
<Folder Include="..\..\..\..\..\..\Dinah%2527s folder\coding\_NET\Visual Studio 2019\Libation\AaxDecrypter\UNTESTED\BytesCrackerLib\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="BytesCrackerLib\alglib1.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_0_10000x789935_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_1_10000x791425_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_2_10000x790991_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_3_10000x792120_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_4_10000x790743_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_5_10000x790568_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_6_10000x791458_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_7_10000x791707_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_8_10000x790202_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\audible_byte#4-4_9_10000x791022_0.rtc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\ffmpeg.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\ffprobe.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="BytesCrackerLib\rcrack.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\AtomicParsley.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avcodec-57.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avdevice-57.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avfilter-6.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avformat-57.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avutil-55.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygcrypto-1.0.0.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cyggcc_s-1.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygmp4v2-2.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygstdc++-6.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygwin1.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygz.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\ffmpeg.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\ffprobe.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\mp4trackdump.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\postproc-54.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\swresample-2.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\swscale-4.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\taglib-sharp.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,346 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Diagnostics;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.StepRunner;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public interface ISimpleAaxToM4bConverter
|
||||
{
|
||||
event EventHandler<int> DecryptProgressUpdate;
|
||||
|
||||
bool Run();
|
||||
|
||||
string AppName { get; set; }
|
||||
string inputFileName { get; }
|
||||
byte[] coverBytes { get; }
|
||||
string outDir { get; }
|
||||
string outputFileName { get; }
|
||||
|
||||
Chapters chapters { get; }
|
||||
Tags tags { get; }
|
||||
EncodingInfo encodingInfo { get; }
|
||||
|
||||
void SetOutputFilename(string outFileName);
|
||||
}
|
||||
public interface IAdvancedAaxToM4bConverter : ISimpleAaxToM4bConverter
|
||||
{
|
||||
bool Step1_CreateDir();
|
||||
bool Step2_DecryptAax();
|
||||
bool Step3_Chapterize();
|
||||
bool Step4_InsertCoverArt();
|
||||
bool Step5_Cleanup();
|
||||
bool Step6_AddTags();
|
||||
bool End_CreateCue();
|
||||
bool End_CreateNfo();
|
||||
}
|
||||
/// <summary>full c# app. integrated logging. no UI</summary>
|
||||
public class AaxToM4bConverter : IAdvancedAaxToM4bConverter
|
||||
{
|
||||
public event EventHandler<int> DecryptProgressUpdate;
|
||||
|
||||
public string inputFileName { get; }
|
||||
public string decryptKey { get; private set; }
|
||||
|
||||
private StepSequence steps { get; }
|
||||
public byte[] coverBytes { get; private set; }
|
||||
|
||||
public string AppName { get; set; } = nameof(AaxToM4bConverter);
|
||||
|
||||
public string outDir { get; private set; }
|
||||
public string outputFileName { get; private set; }
|
||||
|
||||
public Chapters chapters { get; private set; }
|
||||
public Tags tags { get; private set; }
|
||||
public EncodingInfo encodingInfo { get; private set; }
|
||||
|
||||
public static async Task<AaxToM4bConverter> CreateAsync(string inputFile, string decryptKey, Chapters chapters = null)
|
||||
{
|
||||
var converter = new AaxToM4bConverter(inputFile, decryptKey);
|
||||
converter.chapters = chapters ?? new AAXChapters(inputFile);
|
||||
await converter.prelimProcessing();
|
||||
converter.printPrelim();
|
||||
|
||||
return converter;
|
||||
}
|
||||
private AaxToM4bConverter(string inputFile, string decryptKey)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
getKey();
|
||||
return decrypt(tempRipFile);
|
||||
}
|
||||
private void getKey()
|
||||
{
|
||||
Console.WriteLine("Discovering decrypt key");
|
||||
|
||||
Console.WriteLine("Getting file hash");
|
||||
var checksum = BytesCracker.GetChecksum(inputFileName);
|
||||
Console.WriteLine("File hash calculated: " + checksum);
|
||||
|
||||
Console.WriteLine("Cracking activation bytes");
|
||||
var activation_bytes = BytesCracker.GetActivationBytes(checksum);
|
||||
decryptKey = activation_bytes;
|
||||
Console.WriteLine("Activation bytes cracked. Decrypt key: " + activation_bytes);
|
||||
}
|
||||
|
||||
private int decrypt(string tempRipFile)
|
||||
{
|
||||
FileExt.SafeDelete(tempRipFile);
|
||||
|
||||
Console.WriteLine("Decrypting with key " + decryptKey);
|
||||
|
||||
var returnCode = 100;
|
||||
var thread = new Thread(() => returnCode = 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,52 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Diagnostics;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public static class BytesCracker
|
||||
{
|
||||
public static string GetChecksum(string aaxPath)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = BytesCrackerSupportLibraries.ffprobePath,
|
||||
Arguments = aaxPath.SurroundWithQuotes(),
|
||||
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||
};
|
||||
|
||||
// checksum is in the debug info. ffprobe's debug info is written to stderr, not stdout
|
||||
var ffprobeStderr = info.RunHidden().Error;
|
||||
|
||||
// example checksum line:
|
||||
// ... [aax] file checksum == 0c527840c4f18517157eb0b4f9d6f9317ce60cd1
|
||||
var checksum = ffprobeStderr.ExtractString("file checksum == ", 40);
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>use checksum to get activation bytes. activation bytes are unique per audible customer. only have to do this 1x/customer</summary>
|
||||
public static string GetActivationBytes(string checksum)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = BytesCrackerSupportLibraries.rcrackPath,
|
||||
Arguments = @". -h " + checksum,
|
||||
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||
};
|
||||
|
||||
var rcrackStdout = info.RunHidden().Output;
|
||||
|
||||
// example result
|
||||
// 0c527840c4f18517157eb0b4f9d6f9317ce60cd1 \xbd\x89X\x09 hex:bd895809
|
||||
var activation_bytes = rcrackStdout.ExtractString("hex:", 8);
|
||||
|
||||
return activation_bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public static class BytesCrackerSupportLibraries
|
||||
{
|
||||
// GetActivationBytes dependencies
|
||||
// rcrack.exe
|
||||
// alglib1.dll
|
||||
// RainbowCrack files to recover your own Audible activation data (activation_bytes) in an offline manner
|
||||
// audible_byte#4-4_0_10000x789935_0.rtc
|
||||
// audible_byte#4-4_1_10000x791425_0.rtc
|
||||
// audible_byte#4-4_2_10000x790991_0.rtc
|
||||
// audible_byte#4-4_3_10000x792120_0.rtc
|
||||
// audible_byte#4-4_4_10000x790743_0.rtc
|
||||
// audible_byte#4-4_5_10000x790568_0.rtc
|
||||
// audible_byte#4-4_6_10000x791458_0.rtc
|
||||
// audible_byte#4-4_7_10000x791707_0.rtc
|
||||
// audible_byte#4-4_8_10000x790202_0.rtc
|
||||
// audible_byte#4-4_9_10000x791022_0.rtc
|
||||
|
||||
private static string appPath_ { get; } = Path.GetDirectoryName(Dinah.Core.Exe.FileLocationOnDisk);
|
||||
private static string bytesCrackerLib_ { get; } = Path.Combine(appPath_, "BytesCrackerLib");
|
||||
|
||||
public static string ffprobePath { get; } = Path.Combine(bytesCrackerLib_, "ffprobe.exe");
|
||||
public static string rcrackPath { get; } = Path.Combine(bytesCrackerLib_, "rcrack.exe");
|
||||
}
|
||||
}
|
||||
@@ -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/
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"// this connection string is ONLY used for DataLayer's Migrations. this appsettings.json file is NOT used at all by application; it is overwritten": "",
|
||||
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;"
|
||||
}
|
||||
}
|
||||
79
Documentation/Advanced.md
Normal file
@@ -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,205 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
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 chapters = await downloadChapterNames(libraryBook);
|
||||
|
||||
var outputAudioFilename = await aaxToM4bConverterDecrypt(aaxFilename, libraryBook, chapters);
|
||||
|
||||
// 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 async Task<string> aaxToM4bConverterDecrypt(string aaxFilename, LibraryBook libraryBook, Chapters chapters = null)
|
||||
{
|
||||
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, 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 async Task<Chapters> downloadChapterNames(LibraryBook libraryBook)
|
||||
{
|
||||
try
|
||||
{
|
||||
var api = await AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
if (contentMetadata?.ChapterInfo != null)
|
||||
return new DownloadedChapters(contentMetadata.ChapterInfo);
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||