Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da64ceb23 | ||
|
|
3077b854d7 | ||
|
|
a8317d900b | ||
|
|
668bf3a15e | ||
|
|
481e52b503 | ||
|
|
bbb65ec804 | ||
|
|
4b7b646692 | ||
|
|
65cfb1ccbc | ||
|
|
9e9d7b3642 | ||
|
|
1294672809 | ||
|
|
857584a929 | ||
|
|
b4772d9b66 | ||
|
|
382cd9458f | ||
|
|
be7ac7e163 | ||
|
|
9f260128cf | ||
|
|
cdd38350fe | ||
|
|
aefed40a2f | ||
|
|
bd0071354c | ||
|
|
c6bf62a6eb | ||
|
|
95544ef885 | ||
|
|
fe4db16a7e | ||
|
|
9b127a9f97 | ||
|
|
9e4aeac16d | ||
|
|
9653308300 | ||
|
|
7671a1d5c8 | ||
|
|
bd6b5ac873 | ||
|
|
00dc60da79 | ||
|
|
708861bcd5 | ||
|
|
24a2c0a5cf | ||
|
|
1e1d3a7c81 | ||
|
|
2ebddcfee3 | ||
|
|
4517f5b0cb | ||
|
|
666015e867 | ||
|
|
9d5e9b28ae | ||
|
|
6f1e78b9a3 | ||
|
|
d5aa345da6 | ||
|
|
e060c62742 | ||
|
|
5695268d1b | ||
|
|
cea10d3945 | ||
|
|
fe21d15194 | ||
|
|
369d5059c3 | ||
|
|
56a9c62421 | ||
|
|
50d703e2d8 | ||
|
|
5aefc893de | ||
|
|
6e5d9d7657 | ||
|
|
1be7dfcdca | ||
|
|
e58eb8dac1 | ||
|
|
d02eb842c7 | ||
|
|
f3efef1f60 | ||
|
|
659481c83b | ||
|
|
bad4c01474 | ||
|
|
cd53d57516 | ||
|
|
ac54899415 | ||
|
|
abe46e1862 | ||
|
|
3be47d6e57 | ||
|
|
25f608d109 | ||
|
|
aca5324914 | ||
|
|
d8f9e2feb2 | ||
|
|
ed26d34961 | ||
|
|
50919d4721 | ||
|
|
5edde53fed | ||
|
|
1c15e10ac0 | ||
|
|
52be2ad4a2 | ||
|
|
43fdef5e53 | ||
|
|
b699796236 | ||
|
|
b82d03c12c | ||
|
|
b8eb557761 | ||
|
|
ba40c6693e | ||
|
|
e064a3ec2b | ||
|
|
f6561456f7 | ||
|
|
deb91996a8 | ||
|
|
cd1fe6dde0 | ||
|
|
3f6a466ca7 | ||
|
|
50f4fe6546 | ||
|
|
861277460d | ||
|
|
83babf88fc | ||
|
|
58735caff3 | ||
|
|
844eb6c379 | ||
|
|
1b8410e786 | ||
|
|
fed9eff7b8 | ||
|
|
bf5de95fdc | ||
|
|
2e25e797dd | ||
|
|
c1ac68e2ae | ||
|
|
623a92ebec | ||
|
|
2edcd16119 | ||
|
|
af202bd6d1 | ||
|
|
d5a332a84e | ||
|
|
d6884164a5 | ||
|
|
707556a36d | ||
|
|
356a4f588e | ||
|
|
241b31458d | ||
|
|
efb04ddd8f | ||
|
|
781ca7a3f3 | ||
|
|
6f6d8df431 | ||
|
|
9ebde57fc3 | ||
|
|
8dc1e9bfa4 | ||
|
|
e3db2796c9 | ||
|
|
42ef049b63 | ||
|
|
8930426f2a | ||
|
|
ceffa3a108 | ||
|
|
c0a06eec46 | ||
|
|
86ef1d47e8 | ||
|
|
b835aa660a | ||
|
|
4d92d7da3a | ||
|
|
d1e6a85803 | ||
|
|
d8bad7394a | ||
|
|
f5062d1354 | ||
|
|
1d79d9ded2 | ||
|
|
10e212fcde | ||
|
|
1fa267ce1b | ||
|
|
abcf9a1808 | ||
|
|
d4cfad6363 | ||
|
|
3a24561ca2 | ||
|
|
5c19a8aacc | ||
|
|
3946ef8f0d | ||
|
|
6a2868c68d | ||
|
|
7ae3255ea9 | ||
|
|
91e6d94c83 | ||
|
|
e615073893 | ||
|
|
32e27712f0 | ||
|
|
ff39fac50f | ||
|
|
80d3ba42b4 | ||
|
|
d25cfb7499 | ||
|
|
24ad15be11 | ||
|
|
a35c635987 | ||
|
|
aa9fdd2ada | ||
|
|
afa585eb65 | ||
|
|
03acb77162 | ||
|
|
28ca39ca13 | ||
|
|
b05c17b8bb | ||
|
|
3839af65c4 | ||
|
|
275e073c42 | ||
|
|
c4876010e9 | ||
|
|
bce69b2dfc | ||
|
|
fb97e39d9f | ||
|
|
913690df78 | ||
|
|
e68a3f18fa | ||
|
|
1534827ad7 | ||
|
|
b36422bc05 | ||
|
|
1e2335dea0 | ||
|
|
e052590270 | ||
|
|
41ca85268f | ||
|
|
0e9709f304 | ||
|
|
ceec1051d5 | ||
|
|
81ccc11c9c | ||
|
|
702e96ddd6 | ||
|
|
53603f79bd | ||
|
|
250cafe814 | ||
|
|
16633ef1d3 | ||
|
|
f0a5225524 | ||
|
|
a32b36bf81 | ||
|
|
302679719d | ||
|
|
a63baa1758 | ||
|
|
752192a057 | ||
|
|
e245a147d9 | ||
|
|
0ec2bcd897 | ||
|
|
19c75d6790 | ||
|
|
0654156dd6 | ||
|
|
e4b0434733 | ||
|
|
b32721c293 | ||
|
|
c5e39a7523 | ||
|
|
0db837c0ce | ||
|
|
53dae32897 | ||
|
|
018f3ca250 | ||
|
|
a1a8a0cf29 | ||
|
|
f9c3e751f6 | ||
|
|
c7b057ec51 | ||
|
|
85ea762054 | ||
|
|
56cd62ed90 | ||
|
|
9a8c342e61 | ||
|
|
3b81ea37f4 | ||
|
|
3c8bfc31e4 | ||
|
|
59881367fe | ||
|
|
c8ebaee0f7 | ||
|
|
d5d0ad50fa | ||
|
|
6ef792d65d | ||
|
|
20fa9f5523 | ||
|
|
616cc2018a | ||
|
|
e69b1adccd | ||
|
|
280efad939 | ||
|
|
15ec6bec95 | ||
|
|
aae81c3d24 | ||
|
|
259ac94b93 | ||
|
|
a27314464a | ||
|
|
87f07003f4 | ||
|
|
1bf065fd04 | ||
|
|
f8de7e75cc | ||
|
|
7d586b3745 | ||
|
|
a56e071a19 | ||
|
|
480aecb16c | ||
|
|
ec7803fa76 | ||
|
|
91938fd18a | ||
|
|
aa2d3d2b36 | ||
|
|
d5e9cdc5b7 | ||
|
|
305e75c0ae | ||
|
|
0d247fef6a | ||
|
|
94da61c57e | ||
|
|
430ccd9ab1 | ||
|
|
47d94d9bd6 | ||
|
|
a6d1f6039d | ||
|
|
31234a4b98 | ||
|
|
476275ea53 | ||
|
|
792d4a65bc | ||
|
|
0e2dca5f4d | ||
|
|
557296b7be | ||
|
|
2236191263 | ||
|
|
1138c86868 | ||
|
|
08500c66a8 | ||
|
|
67836006c5 | ||
|
|
fa03a9ee25 | ||
|
|
bf4564e365 | ||
|
|
6a96664381 | ||
|
|
e6e3032f02 | ||
|
|
6d424f0a30 | ||
|
|
a6f0375db3 | ||
|
|
1833e8fdb4 | ||
|
|
70151c2e11 | ||
|
|
ab69962e8a | ||
|
|
075fe994af | ||
|
|
1aca1b9fdd | ||
|
|
9fea5a55f4 | ||
|
|
1a0bf45c34 | ||
|
|
67874e07b6 | ||
|
|
ced0bcfe33 | ||
|
|
8c0aa79f78 | ||
|
|
93e8c5be32 | ||
|
|
d9f86aecd2 | ||
|
|
e9552fedef | ||
|
|
84e1c6e809 | ||
|
|
aadd6fd7e0 | ||
|
|
59987ed359 | ||
|
|
a79600c81f | ||
|
|
e8a4c3c6b9 | ||
|
|
62fce26651 | ||
|
|
25add6511f | ||
|
|
b2a28cd39a | ||
|
|
6de3aebf3a | ||
|
|
1b7e422772 | ||
|
|
b0f48b7f00 | ||
|
|
94b07d05c1 | ||
|
|
4392fec7f5 | ||
|
|
c1d0ec076b | ||
|
|
a47d6cd937 | ||
|
|
dcdb5e2a9e | ||
|
|
240325dc18 | ||
|
|
e599d2ec05 | ||
|
|
c62a417cbf | ||
|
|
98fc1ee0a7 | ||
|
|
78d0fd811b | ||
|
|
54d06e580d | ||
|
|
a62fca1e55 | ||
|
|
7b1d23eba4 | ||
|
|
2502c279fc | ||
|
|
878fc8c2fd | ||
|
|
64902eebcb | ||
|
|
e520f6afc4 | ||
|
|
0cc1ae9324 | ||
|
|
1b2124aa35 | ||
|
|
bd4fde9e63 | ||
|
|
c85cfa90a4 | ||
|
|
a0449b50c8 | ||
|
|
010c0bfc7d | ||
|
|
394b063b8c | ||
|
|
251780bed6 | ||
|
|
decc2a9c79 | ||
|
|
b62d84424e | ||
|
|
e15324d2cd | ||
|
|
20b84a9459 | ||
|
|
109198a374 | ||
|
|
dafc68f049 | ||
|
|
3e5c944365 | ||
|
|
d95838309e | ||
|
|
4745fc0db1 | ||
|
|
b3648187ff | ||
|
|
b35483edf8 | ||
|
|
f29088e854 | ||
|
|
b009dfe4ee | ||
|
|
eba94f430c | ||
|
|
06347b7e3c | ||
|
|
6bc426b21d | ||
|
|
e0fac8d2c0 | ||
|
|
4a849aab7f | ||
|
|
ad2d0c84e4 | ||
|
|
6bf360fbfb | ||
|
|
380292a8aa | ||
|
|
f9949da745 | ||
|
|
b0cc52e0aa | ||
|
|
9ef705650b | ||
|
|
c6dadbe75e | ||
|
|
0a65c05083 | ||
|
|
21026340ad | ||
|
|
eb2273247f | ||
|
|
02fc698f1c | ||
|
|
8dc11e89bd | ||
|
|
12c3264000 | ||
|
|
b34672ed19 | ||
|
|
541c8c4b93 | ||
|
|
b97373bf37 | ||
|
|
c0b561cb5a | ||
|
|
bd3ccfd0a9 | ||
|
|
5470acd3af | ||
|
|
64696fe973 | ||
|
|
f2cd93dc54 | ||
|
|
ed2fa4cdd8 | ||
|
|
c2c4636961 | ||
|
|
03cca1d1c0 | ||
|
|
354b075885 | ||
|
|
de7f039b99 | ||
|
|
c2e530af17 | ||
|
|
19797d5d23 | ||
|
|
144c2b5fc7 | ||
|
|
577b525508 | ||
|
|
7da40d1c2e | ||
|
|
4e3a5ca33b | ||
|
|
97e4707f72 | ||
|
|
541dd6b579 | ||
|
|
450ee43677 | ||
|
|
e57229e670 | ||
|
|
c0b2e886d2 | ||
|
|
bb4749c65b | ||
|
|
1ef2a96465 | ||
|
|
5aa37c68a2 | ||
|
|
6e5a1a1f4d | ||
|
|
b624ce16c3 | ||
|
|
3be5b5f919 | ||
|
|
04ac5b69ac | ||
|
|
9cc14ac5c7 | ||
|
|
755eb1405b | ||
|
|
0138294b36 | ||
|
|
c0a4addf30 | ||
|
|
39bbee0eeb | ||
|
|
39dda3f534 | ||
|
|
1cb8dbe795 | ||
|
|
85f1ca6101 | ||
|
|
e13820bbf0 | ||
|
|
3973362457 | ||
|
|
2f3f13afb9 | ||
|
|
b38877e193 | ||
|
|
0e1dbb5377 | ||
|
|
f07cc8b103 | ||
|
|
67775bc797 | ||
|
|
05933a5f0c | ||
|
|
e15ebd5aac | ||
|
|
d32b2ca524 | ||
|
|
d0a895628e | ||
|
|
90f2b3fb21 | ||
|
|
0f95800dde | ||
|
|
ff99cd2456 | ||
|
|
04971f8672 | ||
|
|
b6177b27f4 | ||
|
|
ae97e87506 | ||
|
|
df91ca500f | ||
|
|
2d73f541c0 | ||
|
|
7561eabe52 | ||
|
|
067fb1b0b7 | ||
|
|
78071841cc | ||
|
|
fac232229e | ||
|
|
a43021e87c | ||
|
|
eb599373ee | ||
|
|
7b529b5f9e | ||
|
|
1a0de3b3cf | ||
|
|
947154dc9c | ||
|
|
8acd1f1fe4 | ||
|
|
665210e506 | ||
|
|
91b9370a21 | ||
|
|
ca0aa600b4 | ||
|
|
1c42c4c969 | ||
|
|
389f0ae452 | ||
|
|
6e406311c3 | ||
|
|
00a24756f4 | ||
|
|
9025f312cc | ||
|
|
d5e453edd3 | ||
|
|
26cc64bdd6 | ||
|
|
87884c1af2 | ||
|
|
c0623a949c | ||
|
|
42a23ea23c | ||
|
|
710c31d1ca | ||
|
|
f637ba4dad | ||
|
|
80d2c60cef | ||
|
|
b17d71e4c3 | ||
|
|
3c5bd3a605 | ||
|
|
7cc8d1e693 | ||
|
|
99520d54a5 | ||
|
|
e10a8c078b | ||
|
|
7a196fed7c | ||
|
|
c418c7e725 | ||
|
|
e35748deff | ||
|
|
b57efbe31c | ||
|
|
8f91437701 | ||
|
|
a7eb547ca4 | ||
|
|
0ceb12f74f | ||
|
|
4960624de7 | ||
|
|
2c92f24d89 | ||
|
|
0b7c679dba | ||
|
|
aab3cdc58a | ||
|
|
3c63f5b204 | ||
|
|
0b97bbf827 | ||
|
|
970dbb0c59 | ||
|
|
d21d3c2ceb | ||
|
|
0f6493b8ce | ||
|
|
f26beec1d3 | ||
|
|
c4225e29ed | ||
|
|
9243326cd3 | ||
|
|
86beb8023a | ||
|
|
f4e134742b | ||
|
|
2b17bf4114 | ||
|
|
c4f0fc8f7b | ||
|
|
3d6c836e7d | ||
|
|
c279055af4 | ||
|
|
657cba042a | ||
|
|
7a58e48cae | ||
|
|
670eab62bf | ||
|
|
fc85586809 | ||
|
|
837fc4988d | ||
|
|
4664b47851 | ||
|
|
9864d932e0 | ||
|
|
436f60caa9 | ||
|
|
e9530eda9d | ||
|
|
f4ddac16f9 | ||
|
|
33bdc07f55 | ||
|
|
130a4ed2d3 | ||
|
|
59ebc1af8a | ||
|
|
82e15d2e98 | ||
|
|
9c842f1895 | ||
|
|
8cc849488b | ||
|
|
fcaa232967 | ||
|
|
d3f8153b90 | ||
|
|
99ecf22790 | ||
|
|
b1b7ee65b4 | ||
|
|
6889456662 | ||
|
|
081553dc9f | ||
|
|
5d8d796807 | ||
|
|
f3d88fc746 | ||
|
|
d87ccae6c9 | ||
|
|
2760a7816d | ||
|
|
8f5c649d0f | ||
|
|
fcf9e7a1ef | ||
|
|
2be7b6480a | ||
|
|
f5ded86c02 | ||
|
|
8abfaf0ffd | ||
|
|
b070ba142f | ||
|
|
bd7c6828bf | ||
|
|
d0671ec58c | ||
|
|
1e40ffd1cc | ||
|
|
da2c3e9ed7 | ||
|
|
f62d6abb69 | ||
|
|
12ad7a6322 | ||
|
|
b75247ea3a | ||
|
|
9a963bbe79 | ||
|
|
994bc8b0e4 | ||
|
|
2451605033 | ||
|
|
10942527f3 | ||
|
|
4909ed5ccd | ||
|
|
5cf5df68dc | ||
|
|
dd32cc99ea | ||
|
|
79092dc8eb | ||
|
|
6229e4610a | ||
|
|
bfe36d3f4a | ||
|
|
d42bf44fad | ||
|
|
33e352819c | ||
|
|
53ee0aaee1 | ||
|
|
42707a19bd | ||
|
|
0888706790 | ||
|
|
16453a05f8 | ||
|
|
2fbb7466d3 | ||
|
|
f29f94f45f | ||
|
|
cd973868fc | ||
|
|
3c35f02cac | ||
|
|
22c93e2389 | ||
|
|
8c751eb532 | ||
|
|
4df443e007 | ||
|
|
f52fa41439 | ||
|
|
f77d72fd86 | ||
|
|
9b80c84794 | ||
|
|
725fc658f8 | ||
|
|
24bbf226a1 | ||
|
|
e4e27662f5 | ||
|
|
83474da7b5 | ||
|
|
9146e5f287 | ||
|
|
ff4502c63a | ||
|
|
9711bd8fe1 | ||
|
|
05139e53be | ||
|
|
870b2642a9 | ||
|
|
d31b26ae7d | ||
|
|
5511925ba2 | ||
|
|
f96b20717d | ||
|
|
940c9c45d7 | ||
|
|
87d6008dfc | ||
|
|
b9c0c8d2dc | ||
|
|
81c30d5fd5 | ||
|
|
0aa33d88a5 | ||
|
|
e64a504bb1 | ||
|
|
bc876a159e | ||
|
|
4aa1a838ed | ||
|
|
095a51edd0 | ||
|
|
a3a11bdf3f | ||
|
|
70b503f3d4 | ||
|
|
bbf609b880 | ||
|
|
0992bafe30 | ||
|
|
c810c5275a | ||
|
|
3c1b06872d | ||
|
|
cefdd2f66c | ||
|
|
5dac13b1da | ||
|
|
6014b04b2a | ||
|
|
8aebf48193 | ||
|
|
fbb905957b | ||
|
|
e0ce135838 | ||
|
|
60497c60c1 | ||
|
|
251a77c8b4 | ||
|
|
2b7c1345ee | ||
|
|
69b7d94774 | ||
|
|
7b7494b8a4 | ||
|
|
8fe762709b | ||
|
|
a3f17deb17 | ||
|
|
9390dcdd43 | ||
|
|
e6fb460071 | ||
|
|
6137fdeb33 | ||
|
|
4a4d02ea6a | ||
|
|
be26e5f152 | ||
|
|
127bfba135 | ||
|
|
7efae3c125 | ||
|
|
1e5af21000 | ||
|
|
33a0a4c173 | ||
|
|
eeb7974e05 | ||
|
|
f45ea1a31c | ||
|
|
a866dbaa80 | ||
|
|
62447d6b89 | ||
|
|
93f0724b83 | ||
|
|
724762843d | ||
|
|
ff16afbf0b | ||
|
|
9d7ef25062 | ||
|
|
88078d65e9 | ||
|
|
b07a97c17e | ||
|
|
41e7d65e2a | ||
|
|
7fa5865cf6 | ||
|
|
e0b8fe3b1a | ||
|
|
4a11dd1e16 | ||
|
|
34a474101f | ||
|
|
e6799e7a04 | ||
|
|
0f83f9992c | ||
|
|
d2ad78eb40 | ||
|
|
0b32738f4e | ||
|
|
a9cedb3fca | ||
|
|
51f9cd4bb4 | ||
|
|
a1668e2411 | ||
|
|
b4324cd685 | ||
|
|
6fe4ed3e24 | ||
|
|
bc8bdfe385 | ||
|
|
ee28e3346b | ||
|
|
0cf4c7b7b7 | ||
|
|
123493ee59 | ||
|
|
2d498dd138 | ||
|
|
2343c79126 | ||
|
|
09a5a69f86 | ||
|
|
65929c02bc | ||
|
|
22466d6b98 | ||
|
|
23fe79c618 | ||
|
|
cb7727900c | ||
|
|
bdf6052388 | ||
|
|
99d653eece |
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
+3
-1
@@ -6,10 +6,12 @@ __pycache__/
|
|||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
|
.python-version
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
eggs/
|
eggs/
|
||||||
dist/
|
dist/
|
||||||
|
executable/
|
||||||
build/
|
build/
|
||||||
vendor/
|
vendor/
|
||||||
.eggs/
|
.eggs/
|
||||||
@@ -29,4 +31,4 @@ vendor/
|
|||||||
settings.yaml
|
settings.yaml
|
||||||
gdrive_credentials
|
gdrive_credentials
|
||||||
client_secrets.json
|
client_secrets.json
|
||||||
|
gmail.json
|
||||||
|
|||||||
+2
-2
@@ -41,6 +41,6 @@ Open a new GitHub pull request with the patch. Ensure the PR description clearly
|
|||||||
|
|
||||||
In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consists of more than some lines of code in a local section of Calibre-Webs code. This makes it easier to test it and check all implication before it's made public.
|
In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consists of more than some lines of code in a local section of Calibre-Webs code. This makes it easier to test it and check all implication before it's made public.
|
||||||
|
|
||||||
Please check if your code runs on Python 2.7 (still necessary in 2020) and mainly on python 3. If possible and the feature is related to operating system functions, try to check it on Windows and Linux.
|
Please check if your code runs with python 3, python 2 is no longer supported. If possible and the feature is related to operating system functions, try to check it on Windows and Linux.
|
||||||
Calibre-Web is automatically tested on Linux in combination with python 3.7. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests.
|
Calibre-Web is automatically tested on Linux in combination with python 3.8. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests.
|
||||||
A static code analysis is done by Codacy, but it's partly broken and doesn't run automatically. You could check your code with ESLint before contributing, a configuration file can be found in the projects root folder.
|
A static code analysis is done by Codacy, but it's partly broken and doesn't run automatically. You could check your code with ESLint before contributing, a configuration file can be found in the projects root folder.
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
|
Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
|
||||||
|
|
||||||
|
[](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
|
||||||
|
[]()
|
||||||
|
[](https://github.com/janeczku/calibre-web/releases)
|
||||||
|
[](https://pypi.org/project/calibreweb/)
|
||||||
|
[](https://pypi.org/project/calibreweb/)
|
||||||
|
[](https://discord.gg/h2VsJ2NEfB)
|
||||||
|
|
||||||
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
|
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
|
||||||
|
|
||||||

|

|
||||||
@@ -12,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
|||||||
- full graphical setup
|
- full graphical setup
|
||||||
- User management with fine-grained per-user permissions
|
- User management with fine-grained per-user permissions
|
||||||
- Admin interface
|
- Admin interface
|
||||||
- User Interface in czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, turkish, ukrainian
|
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
|
||||||
- OPDS feed for eBook reader apps
|
- OPDS feed for eBook reader apps
|
||||||
- Filter and search by titles, authors, tags, series and language
|
- Filter and search by titles, authors, tags, series and language
|
||||||
- Create a custom book collection (shelves)
|
- Create a custom book collection (shelves)
|
||||||
@@ -22,7 +29,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
|||||||
- Support for public user registration
|
- Support for public user registration
|
||||||
- Send eBooks to Kindle devices with the click of a button
|
- Send eBooks to Kindle devices with the click of a button
|
||||||
- Sync your Kobo devices through Calibre-Web with your Calibre library
|
- Sync your Kobo devices through Calibre-Web with your Calibre library
|
||||||
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz)
|
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
|
||||||
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
|
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
|
||||||
- Support for Calibre Custom Columns
|
- Support for Calibre Custom Columns
|
||||||
- Ability to hide content based on categories and Custom Column content per user
|
- Ability to hide content based on categories and Custom Column content per user
|
||||||
@@ -32,12 +39,19 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
#### Install via pip
|
||||||
|
1. Install calibre web via pip with the command `pip install calibreweb` (Depending on your OS and or distro the command could also be `pip3`).
|
||||||
|
2. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-Windows) for details
|
||||||
|
3. Calibre-Web can be started afterwards by typing `cps` or `python3 -m cps`
|
||||||
|
|
||||||
|
#### Manual installation
|
||||||
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment.
|
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment.
|
||||||
2. Execute the command: `python3 cps.py` (or `nohup python3 cps.py` - recommended if you want to exit the terminal window)
|
2. Execute the command: `python3 cps.py` (or `nohup python3 cps.py` - recommended if you want to exit the terminal window)
|
||||||
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
|
||||||
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
|
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||||
|
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
|
||||||
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
|
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
|
||||||
5. Go to Login page
|
Go to Login page
|
||||||
|
|
||||||
**Default admin login:**\
|
**Default admin login:**\
|
||||||
*Username:* admin\
|
*Username:* admin\
|
||||||
@@ -48,7 +62,7 @@ Please note that running the above install command can fail on some versions of
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
python 3.x+
|
python 3.5+
|
||||||
|
|
||||||
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
|
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
|
||||||
|
|
||||||
@@ -58,14 +72,7 @@ Optionally, to enable on-the-fly conversion from one ebook format to another whe
|
|||||||
|
|
||||||
## Docker Images
|
## Docker Images
|
||||||
|
|
||||||
Pre-built Docker images are available in these Docker Hub repositories:
|
A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team):
|
||||||
|
|
||||||
#### **Technosoft2000 - x64**
|
|
||||||
+ Docker Hub - [https://hub.docker.com/r/technosoft2000/calibre-web](https://hub.docker.com/r/technosoft2000/calibre-web)
|
|
||||||
+ Github - [https://github.com/Technosoft2000/docker-calibre-web](https://github.com/Technosoft2000/docker-calibre-web)
|
|
||||||
|
|
||||||
Includes the Calibre `ebook-convert` binary.
|
|
||||||
+ The "path to convertertool" should be set to `/opt/calibre/ebook-convert`
|
|
||||||
|
|
||||||
#### **LinuxServer - x64, armhf, aarch64**
|
#### **LinuxServer - x64, armhf, aarch64**
|
||||||
+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
|
+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
|
||||||
@@ -80,7 +87,9 @@ Pre-built Docker images are available in these Docker Hub repositories:
|
|||||||
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
|
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
|
||||||
+ The "path to unrar" should be set to `/usr/bin/unrar`
|
+ The "path to unrar" should be set to `/usr/bin/unrar`
|
||||||
|
|
||||||
# Wiki
|
# Contact
|
||||||
|
|
||||||
|
Just reach us out on [Discord](https://discord.gg/h2VsJ2NEfB)
|
||||||
|
|
||||||
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
||||||
|
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues to ozzie.fernandez.isaacs@googlemail.com
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
To receive fixes for security vulnerabilities it is required to always upgrade to the latest version of Calibre-Web. See https://github.com/janeczku/calibre-web/releases/latest for the latest release.
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
| Fixed in | Description |CVE number |
|
||||||
|
| ---------- |---------|---------|
|
||||||
|
| 3rd July 2018 | Guest access acts as a backdoor||
|
||||||
|
| V 0.6.7 |Hardcoded secret key for sessions |CVE-2020-12627 |
|
||||||
|
| V 0.6.13|Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
||||||
|
| V 0.6.13|Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo||
|
||||||
|
| V 0.6.13|JavaScript could get executed in the description field. Thanks to @ranjit-git ||
|
||||||
|
| V 0.6.13|JavaScript could get executed in a custom column of type "comment" field ||
|
||||||
|
| V 0.6.13|JavaScript could get executed after converting a book to another format with a title containing javascript code||
|
||||||
|
| V 0.6.13|JavaScript could get executed after converting a book to another format with a username containing javascript code||
|
||||||
|
| V 0.6.13|JavaScript could get executed in the description series, categories or publishers title||
|
||||||
|
| V 0.6.13|JavaScript could get executed in the shelf title||
|
||||||
|
| V 0.6.13|Login with the old session cookie after logout. Thanks to @ibarrionuevo||
|
||||||
|
| V 0.6.14|CSRF was possible. Thanks to @mik317 ||
|
||||||
|
| V 0.6.14|Cross-Site Scripting vulnerability on typeahead inputs. Thanks to @notdodo||
|
||||||
|
|
||||||
|
|
||||||
@@ -17,16 +17,11 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
# Insert local directories into path
|
# Insert local directories into path
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))))
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))), 'vendor'))
|
|
||||||
else:
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
|
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
|
||||||
|
|
||||||
@@ -42,13 +37,14 @@ from cps.admin import admi
|
|||||||
from cps.gdrive import gdrive
|
from cps.gdrive import gdrive
|
||||||
from cps.editbooks import editbook
|
from cps.editbooks import editbook
|
||||||
from cps.remotelogin import remotelogin
|
from cps.remotelogin import remotelogin
|
||||||
|
from cps.search_metadata import meta
|
||||||
from cps.error_handler import init_errorhandler
|
from cps.error_handler import init_errorhandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cps.kobo import kobo, get_kobo_activated
|
from cps.kobo import kobo, get_kobo_activated
|
||||||
from cps.kobo_auth import kobo_auth
|
from cps.kobo_auth import kobo_auth
|
||||||
kobo_available = get_kobo_activated()
|
kobo_available = get_kobo_activated()
|
||||||
except ImportError:
|
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
||||||
kobo_available = False
|
kobo_available = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -70,7 +66,7 @@ def main():
|
|||||||
app.register_blueprint(shelf)
|
app.register_blueprint(shelf)
|
||||||
app.register_blueprint(admi)
|
app.register_blueprint(admi)
|
||||||
app.register_blueprint(remotelogin)
|
app.register_blueprint(remotelogin)
|
||||||
# if config.config_use_google_drive:
|
app.register_blueprint(meta)
|
||||||
app.register_blueprint(gdrive)
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
if kobo_available:
|
if kobo_available:
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
|
||||||
|
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
|
||||||
|
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
|
||||||
|
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
|
||||||
|
# apetresc, nanu-c, mutschler, GammaC0de, vuolter
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from flask import session
|
||||||
|
|
||||||
|
|
||||||
|
class MyLoginManager(LoginManager):
|
||||||
|
def _session_protection_failed(self):
|
||||||
|
sess = session._get_current_object()
|
||||||
|
ident = self._session_identifier_generator()
|
||||||
|
if(sess and not (len(sess) == 1 and sess.get('csrf_token', None))) and ident != sess.get('_id', None):
|
||||||
|
return super(). _session_protection_failed()
|
||||||
|
return False
|
||||||
+46
-15
@@ -20,7 +20,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@@ -29,7 +28,7 @@ from babel import Locale as LC
|
|||||||
from babel import negotiate_locale
|
from babel import negotiate_locale
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask import Flask, request, g
|
from flask import Flask, request, g
|
||||||
from flask_login import LoginManager
|
from .MyLoginManager import MyLoginManager
|
||||||
from flask_babel import Babel
|
from flask_babel import Babel
|
||||||
from flask_principal import Principal
|
from flask_principal import Principal
|
||||||
|
|
||||||
@@ -37,6 +36,17 @@ from . import config_sql, logger, cache_buster, cli, ub, db
|
|||||||
from .reverseproxy import ReverseProxied
|
from .reverseproxy import ReverseProxied
|
||||||
from .server import WebServer
|
from .server import WebServer
|
||||||
|
|
||||||
|
try:
|
||||||
|
import lxml
|
||||||
|
lxml_present = True
|
||||||
|
except ImportError:
|
||||||
|
lxml_present = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
wtf_present = True
|
||||||
|
except ImportError:
|
||||||
|
wtf_present = False
|
||||||
|
|
||||||
mimetypes.init()
|
mimetypes.init()
|
||||||
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
||||||
@@ -56,20 +66,29 @@ mimetypes.add_type('application/mp4', '.m4a')
|
|||||||
mimetypes.add_type('application/mp4', '.m4b')
|
mimetypes.add_type('application/mp4', '.m4b')
|
||||||
mimetypes.add_type('application/ogg', '.ogg')
|
mimetypes.add_type('application/ogg', '.ogg')
|
||||||
mimetypes.add_type('application/ogg', '.oga')
|
mimetypes.add_type('application/ogg', '.oga')
|
||||||
|
mimetypes.add_type('text/css', '.css')
|
||||||
|
mimetypes.add_type('text/javascript; charset=UTF-8', '.js')
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE='Lax',
|
SESSION_COOKIE_SAMESITE='Lax',
|
||||||
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
||||||
|
WTF_CSRF_SSL_STRICT=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
lm = LoginManager()
|
lm = MyLoginManager()
|
||||||
lm.login_view = 'web.login'
|
lm.login_view = 'web.login'
|
||||||
lm.anonymous_user = ub.Anonymous
|
lm.anonymous_user = ub.Anonymous
|
||||||
lm.session_protection = 'strong'
|
lm.session_protection = 'strong'
|
||||||
|
|
||||||
|
if wtf_present:
|
||||||
|
csrf = CSRFProtect()
|
||||||
|
csrf.init_app(app)
|
||||||
|
else:
|
||||||
|
csrf = None
|
||||||
|
|
||||||
ub.init_db(cli.settingspath)
|
ub.init_db(cli.settingspath)
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
config = config_sql.load_configuration(ub.session)
|
config = config_sql.load_configuration(ub.session)
|
||||||
@@ -83,25 +102,37 @@ log = logger.create()
|
|||||||
|
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
db.CalibreDB.setup_db(config, cli.settingspath)
|
db.CalibreDB.update_config(config)
|
||||||
|
db.CalibreDB.setup_db(config.config_calibre_dir, cli.settingspath)
|
||||||
|
|
||||||
|
|
||||||
calibre_db = db.CalibreDB()
|
calibre_db = db.CalibreDB()
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
|
||||||
# For python2 convert path to unicode
|
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
app.static_folder = app.static_folder.decode('utf-8')
|
log.info(
|
||||||
app.root_path = app.root_path.decode('utf-8')
|
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
||||||
app.instance_path = app.instance_path.decode('utf-8')
|
print(
|
||||||
|
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
||||||
|
web_server.stop(True)
|
||||||
|
sys.exit(5)
|
||||||
|
if not lxml_present:
|
||||||
|
log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||||
|
print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||||
|
web_server.stop(True)
|
||||||
|
sys.exit(6)
|
||||||
|
if not wtf_present:
|
||||||
|
log.info('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
|
||||||
|
print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
|
||||||
|
web_server.stop(True)
|
||||||
|
sys.exit(7)
|
||||||
|
|
||||||
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
|
|
||||||
if os.environ.get('FLASK_DEBUG'):
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
cache_buster.init_cache_busting(app)
|
cache_buster.init_cache_busting(app)
|
||||||
|
|
||||||
log.info('Starting Calibre Web...')
|
log.info('Starting Calibre Web...')
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
log.info('Python2 is EOL since end of 2019, this version of Calibre-Web supporting Python2 please consider upgrading to Python3')
|
|
||||||
print('Python2 is EOL since end of 2019, this version of Calibre-Web supporting Python2 please consider upgrading to Python3')
|
|
||||||
Principal(app)
|
Principal(app)
|
||||||
lm.init_app(app)
|
lm.init_app(app)
|
||||||
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
||||||
@@ -125,9 +156,8 @@ def create_app():
|
|||||||
def get_locale():
|
def get_locale():
|
||||||
# if a user is logged in, use the locale from the user settings
|
# if a user is logged in, use the locale from the user settings
|
||||||
user = getattr(g, 'user', None)
|
user = getattr(g, 'user', None)
|
||||||
# user = None
|
|
||||||
if user is not None and hasattr(user, "locale"):
|
if user is not None and hasattr(user, "locale"):
|
||||||
if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
|
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
||||||
return user.locale
|
return user.locale
|
||||||
|
|
||||||
preferred = list()
|
preferred = list()
|
||||||
@@ -146,6 +176,7 @@ def get_timezone():
|
|||||||
user = getattr(g, 'user', None)
|
user = getattr(g, 'user', None)
|
||||||
return user.timezone if user else None
|
return user.timezone if user else None
|
||||||
|
|
||||||
|
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
updater_thread.start()
|
updater_thread.start()
|
||||||
|
|||||||
+20
-2
@@ -20,7 +20,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -29,6 +28,10 @@ from collections import OrderedDict
|
|||||||
import babel, pytz, requests, sqlalchemy
|
import babel, pytz, requests, sqlalchemy
|
||||||
import werkzeug, flask, flask_login, flask_principal, jinja2
|
import werkzeug, flask, flask_login, flask_principal, jinja2
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
try:
|
||||||
|
from flask_wtf import __version__ as flaskwtf_version
|
||||||
|
except ImportError:
|
||||||
|
flaskwtf_version = _(u'not installed')
|
||||||
|
|
||||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
@@ -37,6 +40,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from flask_login.__about__ import __version__ as flask_loginVersion
|
from flask_login.__about__ import __version__ as flask_loginVersion
|
||||||
try:
|
try:
|
||||||
|
# pylint: disable=unused-import
|
||||||
import unidecode
|
import unidecode
|
||||||
# _() necessary to make babel aware of string for translation
|
# _() necessary to make babel aware of string for translation
|
||||||
unidecode_version = _(u'installed')
|
unidecode_version = _(u'installed')
|
||||||
@@ -48,6 +52,17 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
flask_danceVersion = None
|
flask_danceVersion = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from greenlet import __version__ as greenlet_Version
|
||||||
|
except ImportError:
|
||||||
|
greenlet_Version = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from scholarly import scholarly
|
||||||
|
scholarly_version = _(u'installed')
|
||||||
|
except ImportError:
|
||||||
|
scholarly_version = _(u'not installed')
|
||||||
|
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
about = flask.Blueprint('about', __name__)
|
about = flask.Blueprint('about', __name__)
|
||||||
@@ -63,6 +78,7 @@ _VERSIONS = OrderedDict(
|
|||||||
Flask=flask.__version__,
|
Flask=flask.__version__,
|
||||||
Flask_Login=flask_loginVersion,
|
Flask_Login=flask_loginVersion,
|
||||||
Flask_Principal=flask_principal.__version__,
|
Flask_Principal=flask_principal.__version__,
|
||||||
|
Flask_WTF=flaskwtf_version,
|
||||||
Werkzeug=werkzeug.__version__,
|
Werkzeug=werkzeug.__version__,
|
||||||
Babel=babel.__version__,
|
Babel=babel.__version__,
|
||||||
Jinja2=jinja2.__version__,
|
Jinja2=jinja2.__version__,
|
||||||
@@ -73,11 +89,13 @@ _VERSIONS = OrderedDict(
|
|||||||
iso639=isoLanguages.__version__,
|
iso639=isoLanguages.__version__,
|
||||||
pytz=pytz.__version__,
|
pytz=pytz.__version__,
|
||||||
Unidecode=unidecode_version,
|
Unidecode=unidecode_version,
|
||||||
|
Scholarly=scholarly_version,
|
||||||
Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
|
Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
|
||||||
python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
|
python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
|
||||||
Goodreads=u'installed' if bool(services.goodreads_support) else None,
|
Goodreads=u'installed' if bool(services.goodreads_support) else None,
|
||||||
jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||||
flask_dance = flask_danceVersion
|
flask_dance=flask_danceVersion,
|
||||||
|
greenlet=greenlet_Version
|
||||||
)
|
)
|
||||||
_VERSIONS.update(uploader.get_versions())
|
_VERSIONS.update(uploader.get_versions())
|
||||||
|
|
||||||
|
|||||||
+703
-276
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -19,7 +19,6 @@
|
|||||||
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
|
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
|
||||||
# Uses query strings so CSS font files are found without having to resort to absolute URLs
|
# Uses query strings so CSS font files are found without having to resort to absolute URLs
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ def init_cache_busting(app):
|
|||||||
# compute version component
|
# compute version component
|
||||||
rooted_filename = os.path.join(dirpath, filename)
|
rooted_filename = os.path.join(dirpath, filename)
|
||||||
with open(rooted_filename, 'rb') as f:
|
with open(rooted_filename, 'rb') as f:
|
||||||
file_hash = hashlib.md5(f.read()).hexdigest()[:7]
|
file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec
|
||||||
|
|
||||||
# save version to tables
|
# save version to tables
|
||||||
file_path = rooted_filename.replace(static_folder, "")
|
file_path = rooted_filename.replace(static_folder, "")
|
||||||
@@ -64,6 +63,7 @@ def init_cache_busting(app):
|
|||||||
return filename.split("?", 1)[0]
|
return filename.split("?", 1)[0]
|
||||||
|
|
||||||
@app.url_defaults
|
@app.url_defaults
|
||||||
|
# pylint: disable=unused-variable
|
||||||
def reverse_to_cache_busted_url(endpoint, values):
|
def reverse_to_cache_busted_url(endpoint, values):
|
||||||
"""
|
"""
|
||||||
Make `url_for` produce busted filenames when using the 'static' endpoint.
|
Make `url_for` produce busted filenames when using the 'static' endpoint.
|
||||||
|
|||||||
+12
-26
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
@@ -45,22 +44,9 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
|
|||||||
version=version_info())
|
version=version_info())
|
||||||
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
||||||
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
|
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
|
||||||
parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode')
|
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
if args.p:
|
|
||||||
args.p = args.p.decode('utf-8')
|
|
||||||
if args.g:
|
|
||||||
args.g = args.g.decode('utf-8')
|
|
||||||
if args.k:
|
|
||||||
args.k = args.k.decode('utf-8')
|
|
||||||
if args.c:
|
|
||||||
args.c = args.c.decode('utf-8')
|
|
||||||
if args.s:
|
|
||||||
args.s = args.s.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
|
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
|
||||||
gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db")
|
gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db")
|
||||||
|
|
||||||
@@ -91,29 +77,29 @@ if (args.k and not args.c) or (not args.k and args.c):
|
|||||||
if args.k == "":
|
if args.k == "":
|
||||||
keyfilepath = ""
|
keyfilepath = ""
|
||||||
|
|
||||||
# handle and check ipadress argument
|
# handle and check ip address argument
|
||||||
ipadress = args.i or None
|
ip_address = args.i or None
|
||||||
if ipadress:
|
if ip_address:
|
||||||
try:
|
try:
|
||||||
# try to parse the given ip address with socket
|
# try to parse the given ip address with socket
|
||||||
if hasattr(socket, 'inet_pton'):
|
if hasattr(socket, 'inet_pton'):
|
||||||
if ':' in ipadress:
|
if ':' in ip_address:
|
||||||
socket.inet_pton(socket.AF_INET6, ipadress)
|
socket.inet_pton(socket.AF_INET6, ip_address)
|
||||||
else:
|
else:
|
||||||
socket.inet_pton(socket.AF_INET, ipadress)
|
socket.inet_pton(socket.AF_INET, ip_address)
|
||||||
else:
|
else:
|
||||||
# on windows python < 3.4, inet_pton is not available
|
# on windows python < 3.4, inet_pton is not available
|
||||||
# inet_atom only handles IPv4 addresses
|
# inet_atom only handles IPv4 addresses
|
||||||
socket.inet_aton(ipadress)
|
socket.inet_aton(ip_address)
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
print(ipadress, ':', err)
|
print(ip_address, ':', err)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# handle and check user password argument
|
# handle and check user password argument
|
||||||
user_credentials = args.s or None
|
user_credentials = args.s or None
|
||||||
if user_credentials and ":" not in user_credentials:
|
if user_credentials and ":" not in user_credentials:
|
||||||
print("No valid username:password format")
|
print("No valid 'username:password' format")
|
||||||
sys.exit(3)
|
sys.exit(3)
|
||||||
|
|
||||||
# Handles enableing of filepicker
|
if args.f:
|
||||||
filepicker = args.f or None
|
print("Warning: -f flag is depreciated and will be removed in next version")
|
||||||
|
|||||||
+26
-19
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from . import logger, isoLanguages
|
from . import logger, isoLanguages
|
||||||
@@ -74,19 +73,8 @@ def _cover_processing(tmp_file_name, img, extension):
|
|||||||
return tmp_cover_name
|
return tmp_cover_name
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable):
|
||||||
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
cover_data = None
|
||||||
cover_data = extension = None
|
|
||||||
if use_comic_meta:
|
|
||||||
archive = ComicArchive(tmp_file_name, rar_exe_path=rarExecutable)
|
|
||||||
for index, name in enumerate(archive.getPageNameList()):
|
|
||||||
ext = os.path.splitext(name)
|
|
||||||
if len(ext) > 1:
|
|
||||||
extension = ext[1].lower()
|
|
||||||
if extension in COVER_EXTENSIONS:
|
|
||||||
cover_data = archive.getPage(index)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if original_file_extension.upper() == '.CBZ':
|
if original_file_extension.upper() == '.CBZ':
|
||||||
cf = zipfile.ZipFile(tmp_file_name)
|
cf = zipfile.ZipFile(tmp_file_name)
|
||||||
for name in cf.namelist():
|
for name in cf.namelist():
|
||||||
@@ -116,8 +104,24 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
|||||||
if extension in COVER_EXTENSIONS:
|
if extension in COVER_EXTENSIONS:
|
||||||
cover_data = cf.read(name)
|
cover_data = cf.read(name)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug('Rarfile failed with error: %s', e)
|
log.debug('Rarfile failed with error: %s', ex)
|
||||||
|
return cover_data
|
||||||
|
|
||||||
|
|
||||||
|
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||||
|
cover_data = extension = None
|
||||||
|
if use_comic_meta:
|
||||||
|
archive = ComicArchive(tmp_file_name, rar_exe_path=rarExecutable)
|
||||||
|
for index, name in enumerate(archive.getPageNameList()):
|
||||||
|
ext = os.path.splitext(name)
|
||||||
|
if len(ext) > 1:
|
||||||
|
extension = ext[1].lower()
|
||||||
|
if extension in COVER_EXTENSIONS:
|
||||||
|
cover_data = archive.getPage(index)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
|
||||||
return _cover_processing(tmp_file_name, cover_data, extension)
|
return _cover_processing(tmp_file_name, cover_data, extension)
|
||||||
|
|
||||||
|
|
||||||
@@ -142,13 +146,15 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
|||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
extension=original_file_extension,
|
extension=original_file_extension,
|
||||||
title=loadedMetadata.title or original_file_name,
|
title=loadedMetadata.title or original_file_name,
|
||||||
author=" & ".join([credit["person"] for credit in loadedMetadata.credits if credit["role"] == "Writer"]) or u'Unknown',
|
author=" & ".join([credit["person"]
|
||||||
|
for credit in loadedMetadata.credits if credit["role"] == "Writer"]) or u'Unknown',
|
||||||
cover=_extractCover(tmp_file_path, original_file_extension, rarExecutable),
|
cover=_extractCover(tmp_file_path, original_file_extension, rarExecutable),
|
||||||
description=loadedMetadata.comments or "",
|
description=loadedMetadata.comments or "",
|
||||||
tags="",
|
tags="",
|
||||||
series=loadedMetadata.series or "",
|
series=loadedMetadata.series or "",
|
||||||
series_id=loadedMetadata.issue or "",
|
series_id=loadedMetadata.issue or "",
|
||||||
languages=loadedMetadata.language)
|
languages=loadedMetadata.language,
|
||||||
|
publisher="")
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
@@ -160,4 +166,5 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
|||||||
tags="",
|
tags="",
|
||||||
series="",
|
series="",
|
||||||
series_id="",
|
series_id="",
|
||||||
languages="")
|
languages="",
|
||||||
|
publisher="")
|
||||||
|
|||||||
+51
-34
@@ -16,16 +16,20 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
|
from sqlalchemy.sql.expression import text
|
||||||
|
try:
|
||||||
|
# Compatibility with sqlalchemy 2.0
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
except ImportError:
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
from . import constants, cli, logger, ub
|
from . import constants, cli, logger
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@@ -35,7 +39,7 @@ class _Flask_Settings(_Base):
|
|||||||
__tablename__ = 'flask_settings'
|
__tablename__ = 'flask_settings'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
flask_session_key = Column(BLOB, default="")
|
flask_session_key = Column(BLOB, default=b"")
|
||||||
|
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
self.flask_session_key = key
|
self.flask_session_key = key
|
||||||
@@ -54,19 +58,21 @@ class _Settings(_Base):
|
|||||||
mail_password = Column(String, default='mypassword')
|
mail_password = Column(String, default='mypassword')
|
||||||
mail_from = Column(String, default='automailer <mail@example.com>')
|
mail_from = Column(String, default='automailer <mail@example.com>')
|
||||||
mail_size = Column(Integer, default=25*1024*1024)
|
mail_size = Column(Integer, default=25*1024*1024)
|
||||||
|
mail_server_type = Column(SmallInteger, default=0)
|
||||||
|
mail_gmail_token = Column(JSON, default={})
|
||||||
|
|
||||||
config_calibre_dir = Column(String)
|
config_calibre_dir = Column(String)
|
||||||
config_port = Column(Integer, default=constants.DEFAULT_PORT)
|
config_port = Column(Integer, default=constants.DEFAULT_PORT)
|
||||||
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
|
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
|
||||||
config_certfile = Column(String)
|
config_certfile = Column(String)
|
||||||
config_keyfile = Column(String)
|
config_keyfile = Column(String)
|
||||||
|
config_trustedhosts = Column(String,default='')
|
||||||
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
||||||
config_books_per_page = Column(Integer, default=60)
|
config_books_per_page = Column(Integer, default=60)
|
||||||
config_random_books = Column(Integer, default=4)
|
config_random_books = Column(Integer, default=4)
|
||||||
config_authors_max = Column(Integer, default=0)
|
config_authors_max = Column(Integer, default=0)
|
||||||
config_read_column = Column(Integer, default=0)
|
config_read_column = Column(Integer, default=0)
|
||||||
config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
|
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
|
||||||
config_mature_content_tags = Column(String, default='')
|
config_mature_content_tags = Column(String, default='')
|
||||||
config_theme = Column(Integer, default=0)
|
config_theme = Column(Integer, default=0)
|
||||||
|
|
||||||
@@ -83,6 +89,8 @@ class _Settings(_Base):
|
|||||||
|
|
||||||
config_default_role = Column(SmallInteger, default=0)
|
config_default_role = Column(SmallInteger, default=0)
|
||||||
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
|
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
|
||||||
|
config_default_language = Column(String(3), default="all")
|
||||||
|
config_default_locale = Column(String(2), default="en")
|
||||||
config_columns_to_ignore = Column(String)
|
config_columns_to_ignore = Column(String)
|
||||||
|
|
||||||
config_denied_tags = Column(String, default="")
|
config_denied_tags = Column(String, default="")
|
||||||
@@ -125,6 +133,7 @@ class _Settings(_Base):
|
|||||||
config_calibre = Column(String)
|
config_calibre = Column(String)
|
||||||
config_rarfile_location = Column(String, default=None)
|
config_rarfile_location = Column(String, default=None)
|
||||||
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
|
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
|
||||||
|
config_unicode_filename =Column(Boolean, default=False)
|
||||||
|
|
||||||
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
||||||
|
|
||||||
@@ -146,15 +155,16 @@ class _ConfigSQL(object):
|
|||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
change = False
|
change = False
|
||||||
if self.config_converterpath == None:
|
if self.config_converterpath == None: # pylint: disable=access-member-before-definition
|
||||||
change = True
|
change = True
|
||||||
self.config_converterpath = autodetect_calibre_binary()
|
self.config_converterpath = autodetect_calibre_binary()
|
||||||
|
|
||||||
if self.config_kepubifypath == None:
|
if self.config_kepubifypath == None: # pylint: disable=access-member-before-definition
|
||||||
|
|
||||||
change = True
|
change = True
|
||||||
self.config_kepubifypath = autodetect_kepubify_binary()
|
self.config_kepubifypath = autodetect_kepubify_binary()
|
||||||
|
|
||||||
if self.config_rarfile_location == None:
|
if self.config_rarfile_location == None: # pylint: disable=access-member-before-definition
|
||||||
change = True
|
change = True
|
||||||
self.config_rarfile_location = autodetect_unrar_binary()
|
self.config_rarfile_location = autodetect_unrar_binary()
|
||||||
if change:
|
if change:
|
||||||
@@ -181,8 +191,9 @@ class _ConfigSQL(object):
|
|||||||
return None
|
return None
|
||||||
return self.config_keyfile
|
return self.config_keyfile
|
||||||
|
|
||||||
def get_config_ipaddress(self):
|
@staticmethod
|
||||||
return cli.ipadress or ""
|
def get_config_ipaddress():
|
||||||
|
return cli.ip_address or ""
|
||||||
|
|
||||||
def _has_role(self, role_flag):
|
def _has_role(self, role_flag):
|
||||||
return constants.has_flag(self.config_default_role, role_flag)
|
return constants.has_flag(self.config_default_role, role_flag)
|
||||||
@@ -240,18 +251,18 @@ class _ConfigSQL(object):
|
|||||||
return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')}
|
return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')}
|
||||||
|
|
||||||
def get_mail_server_configured(self):
|
def get_mail_server_configured(self):
|
||||||
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER)
|
return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0)
|
||||||
|
or (self.mail_gmail_token != {} and self.mail_server_type == 1))
|
||||||
|
|
||||||
|
|
||||||
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
|
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
|
||||||
'''Possibly updates a field of this object.
|
"""Possibly updates a field of this object.
|
||||||
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
|
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
|
||||||
|
|
||||||
:returns: `True` if the field has changed value
|
:returns: `True` if the field has changed value
|
||||||
'''
|
"""
|
||||||
new_value = dictionary.get(field, default)
|
new_value = dictionary.get(field, default)
|
||||||
if new_value is None:
|
if new_value is None:
|
||||||
# log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if field not in self.__dict__:
|
if field not in self.__dict__:
|
||||||
@@ -268,7 +279,6 @@ class _ConfigSQL(object):
|
|||||||
if current_value == new_value:
|
if current_value == new_value:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value)
|
|
||||||
setattr(self, field, new_value)
|
setattr(self, field, new_value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -299,6 +309,10 @@ class _ConfigSQL(object):
|
|||||||
have_metadata_db = os.path.isfile(db_file)
|
have_metadata_db = os.path.isfile(db_file)
|
||||||
self.db_configured = have_metadata_db
|
self.db_configured = have_metadata_db
|
||||||
constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
|
constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
|
||||||
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
|
logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG)
|
||||||
|
else:
|
||||||
|
# pylint: disable=access-member-before-definition
|
||||||
logfile = logger.setup(self.config_logfile, self.config_log_level)
|
logfile = logger.setup(self.config_logfile, self.config_log_level)
|
||||||
if logfile != self.config_logfile:
|
if logfile != self.config_logfile:
|
||||||
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
|
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
|
||||||
@@ -334,7 +348,7 @@ class _ConfigSQL(object):
|
|||||||
log.error(error)
|
log.error(error)
|
||||||
log.warning("invalidating configuration")
|
log.warning("invalidating configuration")
|
||||||
self.db_configured = False
|
self.db_configured = False
|
||||||
self.config_calibre_dir = None
|
# self.config_calibre_dir = None
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
@@ -345,26 +359,29 @@ def _migrate_table(session, orm_class):
|
|||||||
if column_name[0] != '_':
|
if column_name[0] != '_':
|
||||||
try:
|
try:
|
||||||
session.query(column).first()
|
session.query(column).first()
|
||||||
except exc.OperationalError as err:
|
except OperationalError as err:
|
||||||
log.debug("%s: %s", column_name, err.args[0])
|
log.debug("%s: %s", column_name, err.args[0])
|
||||||
if column.default is not None:
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
if isinstance(column.default.arg, unicode):
|
|
||||||
column.default.arg = column.default.arg.encode('utf-8')
|
|
||||||
if column.default is None:
|
if column.default is None:
|
||||||
column_default = ""
|
column_default = ""
|
||||||
else:
|
else:
|
||||||
if isinstance(column.default.arg, bool):
|
if isinstance(column.default.arg, bool):
|
||||||
column_default = ("DEFAULT %r" % int(column.default.arg))
|
column_default = "DEFAULT {}".format(int(column.default.arg))
|
||||||
else:
|
else:
|
||||||
column_default = ("DEFAULT %r" % column.default.arg)
|
column_default = "DEFAULT `{}`".format(column.default.arg)
|
||||||
alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
|
if isinstance(column.type, JSON):
|
||||||
|
column_type = "JSON"
|
||||||
|
else:
|
||||||
|
column_type = column.type
|
||||||
|
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
|
||||||
column_name,
|
column_name,
|
||||||
column.type,
|
column_type,
|
||||||
column_default)
|
column_default))
|
||||||
log.debug(alter_table)
|
log.debug(alter_table)
|
||||||
session.execute(alter_table)
|
session.execute(alter_table)
|
||||||
changed = True
|
changed = True
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
log.error("Database corrupt column: {}".format(column_name))
|
||||||
|
log.debug(e)
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
try:
|
try:
|
||||||
@@ -423,12 +440,12 @@ def load_configuration(session):
|
|||||||
session.commit()
|
session.commit()
|
||||||
conf = _ConfigSQL(session)
|
conf = _ConfigSQL(session)
|
||||||
# Migrate from global restrictions to user based restrictions
|
# Migrate from global restrictions to user based restrictions
|
||||||
if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
|
#if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
|
||||||
conf.config_denied_tags = conf.config_mature_content_tags
|
# conf.config_denied_tags = conf.config_mature_content_tags
|
||||||
conf.save()
|
# conf.save()
|
||||||
session.query(ub.User).filter(ub.User.mature_content != True). \
|
# session.query(ub.User).filter(ub.User.mature_content != True). \
|
||||||
update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
|
# update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
|
||||||
session.commit()
|
# session.commit()
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
def get_flask_session_key(session):
|
def get_flask_session_key(session):
|
||||||
|
|||||||
+32
-11
@@ -16,20 +16,21 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from sqlalchemy import __version__ as sql_version
|
||||||
|
|
||||||
HOME_CONFIG = False
|
sqlalchemy_version2 = ([int(x) for x in sql_version.split('.')] >= [2,0,0])
|
||||||
|
|
||||||
|
# if installed via pip this variable is set to true (empty file with name .HOMEDIR present)
|
||||||
|
HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR'))
|
||||||
|
|
||||||
|
#In executables updater is not available, so variable is set to False there
|
||||||
|
UPDATER_AVAILABLE = True
|
||||||
|
|
||||||
# Base dir is parent of current file, necessary if called from different folder
|
# Base dir is parent of current file, necessary if called from different folder
|
||||||
if sys.version_info < (3, 0):
|
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),os.pardir))
|
||||||
BASE_DIR = os.path.abspath(os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)),os.pardir)).decode('utf-8')
|
|
||||||
else:
|
|
||||||
BASE_DIR = os.path.abspath(os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)),os.pardir))
|
|
||||||
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
||||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
||||||
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
||||||
@@ -84,6 +85,26 @@ SIDEBAR_ARCHIVED = 1 << 15
|
|||||||
SIDEBAR_DOWNLOAD = 1 << 16
|
SIDEBAR_DOWNLOAD = 1 << 16
|
||||||
SIDEBAR_LIST = 1 << 17
|
SIDEBAR_LIST = 1 << 17
|
||||||
|
|
||||||
|
sidebar_settings = {
|
||||||
|
"detail_random": DETAIL_RANDOM,
|
||||||
|
"sidebar_language": SIDEBAR_LANGUAGE,
|
||||||
|
"sidebar_series": SIDEBAR_SERIES,
|
||||||
|
"sidebar_category": SIDEBAR_CATEGORY,
|
||||||
|
"sidebar_random": SIDEBAR_RANDOM,
|
||||||
|
"sidebar_author": SIDEBAR_AUTHOR,
|
||||||
|
"sidebar_best_rated": SIDEBAR_BEST_RATED,
|
||||||
|
"sidebar_read_and_unread": SIDEBAR_READ_AND_UNREAD,
|
||||||
|
"sidebar_recent": SIDEBAR_RECENT,
|
||||||
|
"sidebar_sorted": SIDEBAR_SORTED,
|
||||||
|
"sidebar_publisher": SIDEBAR_PUBLISHER,
|
||||||
|
"sidebar_rating": SIDEBAR_RATING,
|
||||||
|
"sidebar_format": SIDEBAR_FORMAT,
|
||||||
|
"sidebar_archived": SIDEBAR_ARCHIVED,
|
||||||
|
"sidebar_download": SIDEBAR_DOWNLOAD,
|
||||||
|
"sidebar_list": SIDEBAR_LIST,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
|
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
|
||||||
ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1
|
ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1
|
||||||
|
|
||||||
@@ -102,7 +123,7 @@ LDAP_AUTH_SIMPLE = 0
|
|||||||
|
|
||||||
DEFAULT_MAIL_SERVER = "mail.example.org"
|
DEFAULT_MAIL_SERVER = "mail.example.org"
|
||||||
|
|
||||||
DEFAULT_PASSWORD = "admin123"
|
DEFAULT_PASSWORD = "admin123" # nosec
|
||||||
DEFAULT_PORT = 8083
|
DEFAULT_PORT = 8083
|
||||||
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
|
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
|
||||||
try:
|
try:
|
||||||
@@ -128,9 +149,9 @@ def selected_roles(dictionary):
|
|||||||
|
|
||||||
# :rtype: BookMeta
|
# :rtype: BookMeta
|
||||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||||
'series_id, languages')
|
'series_id, languages, publisher')
|
||||||
|
|
||||||
STABLE_VERSION = {'version': '0.6.10'}
|
STABLE_VERSION = {'version': '0.6.14'}
|
||||||
|
|
||||||
NIGHTLY_VERSION = {}
|
NIGHTLY_VERSION = {}
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
|
|||||||
+3
-4
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
@@ -39,9 +38,9 @@ def _get_command_version(path, pattern, argument=None):
|
|||||||
if argument:
|
if argument:
|
||||||
command.append(argument)
|
command.append(argument)
|
||||||
try:
|
try:
|
||||||
for line in process_wait(command):
|
match = process_wait(command, pattern=pattern)
|
||||||
if re.search(pattern, line):
|
if isinstance(match, re.Match):
|
||||||
return line
|
return match.string
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.warning("%s: %s", path, ex)
|
log.warning("%s: %s", path, ex)
|
||||||
return _EXECUTION_ERROR
|
return _EXECUTION_ERROR
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -30,7 +29,13 @@ from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
|||||||
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
||||||
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
|
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
|
||||||
from sqlalchemy.orm.collections import InstrumentedList
|
from sqlalchemy.orm.collections import InstrumentedList
|
||||||
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
|
from sqlalchemy.ext.declarative import DeclarativeMeta
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
try:
|
||||||
|
# Compatibility with sqlalchemy 2.0
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
except ImportError:
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
@@ -38,6 +43,7 @@ from flask_login import current_user
|
|||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
from flask import flash
|
||||||
|
|
||||||
from . import logger, ub, isoLanguages
|
from . import logger, ub, isoLanguages
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
@@ -50,7 +56,9 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
use_unidecode = False
|
use_unidecode = False
|
||||||
|
|
||||||
cc_exceptions = ['datetime', 'comments', 'composite', 'series']
|
log = logger.create()
|
||||||
|
|
||||||
|
cc_exceptions = ['composite', 'series']
|
||||||
cc_classes = {}
|
cc_classes = {}
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
@@ -113,6 +121,8 @@ class Identifiers(Base):
|
|||||||
return u"Douban"
|
return u"Douban"
|
||||||
elif format_type == "goodreads":
|
elif format_type == "goodreads":
|
||||||
return u"Goodreads"
|
return u"Goodreads"
|
||||||
|
elif format_type == "babelio":
|
||||||
|
return u"Babelio"
|
||||||
elif format_type == "google":
|
elif format_type == "google":
|
||||||
return u"Google Books"
|
return u"Google Books"
|
||||||
elif format_type == "kobo":
|
elif format_type == "kobo":
|
||||||
@@ -140,6 +150,8 @@ class Identifiers(Base):
|
|||||||
return u"https://dx.doi.org/{0}".format(self.val)
|
return u"https://dx.doi.org/{0}".format(self.val)
|
||||||
elif format_type == "goodreads":
|
elif format_type == "goodreads":
|
||||||
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
|
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
|
||||||
|
elif format_type == "babelio":
|
||||||
|
return u"https://www.babelio.com/livres/titre/{0}".format(self.val)
|
||||||
elif format_type == "douban":
|
elif format_type == "douban":
|
||||||
return u"https://book.douban.com/subject/{0}".format(self.val)
|
return u"https://book.douban.com/subject/{0}".format(self.val)
|
||||||
elif format_type == "google":
|
elif format_type == "google":
|
||||||
@@ -154,10 +166,8 @@ class Identifiers(Base):
|
|||||||
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
||||||
elif format_type == "isfdb":
|
elif format_type == "isfdb":
|
||||||
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
||||||
elif format_type == "url":
|
|
||||||
return u"{0}".format(self.val)
|
|
||||||
else:
|
else:
|
||||||
return u""
|
return u"{0}".format(self.val)
|
||||||
|
|
||||||
|
|
||||||
class Comments(Base):
|
class Comments(Base):
|
||||||
@@ -326,7 +336,6 @@ class Books(Base):
|
|||||||
has_cover = Column(Integer, default=0)
|
has_cover = Column(Integer, default=0)
|
||||||
uuid = Column(String)
|
uuid = Column(String)
|
||||||
isbn = Column(String(collation='NOCASE'), default="")
|
isbn = Column(String(collation='NOCASE'), default="")
|
||||||
# Iccn = Column(String(collation='NOCASE'), default="")
|
|
||||||
flags = Column(Integer, nullable=False, default=1)
|
flags = Column(Integer, nullable=False, default=1)
|
||||||
|
|
||||||
authors = relationship('Authors', secondary=books_authors_link, backref='books')
|
authors = relationship('Authors', secondary=books_authors_link, backref='books')
|
||||||
@@ -377,28 +386,29 @@ class Custom_Columns(Base):
|
|||||||
|
|
||||||
def get_display_dict(self):
|
def get_display_dict(self):
|
||||||
display_dict = ast.literal_eval(self.display)
|
display_dict = ast.literal_eval(self.display)
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']]
|
|
||||||
return display_dict
|
return display_dict
|
||||||
|
|
||||||
|
|
||||||
class AlchemyEncoder(json.JSONEncoder):
|
class AlchemyEncoder(json.JSONEncoder):
|
||||||
|
|
||||||
def default(self, obj):
|
def default(self, o):
|
||||||
if isinstance(obj.__class__, DeclarativeMeta):
|
if isinstance(o.__class__, DeclarativeMeta):
|
||||||
# an SQLAlchemy class
|
# an SQLAlchemy class
|
||||||
fields = {}
|
fields = {}
|
||||||
for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
|
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x != "password"]:
|
||||||
if field == 'books':
|
if field == 'books':
|
||||||
continue
|
continue
|
||||||
data = obj.__getattribute__(field)
|
data = o.__getattribute__(field)
|
||||||
try:
|
try:
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.replace("'", "\'")
|
data = data.replace("'", "\'")
|
||||||
elif isinstance(data, InstrumentedList):
|
elif isinstance(data, InstrumentedList):
|
||||||
el = list()
|
el = list()
|
||||||
|
# ele = None
|
||||||
for ele in data:
|
for ele in data:
|
||||||
if ele.get:
|
if hasattr(ele, 'value'): # converter for custom_column values
|
||||||
|
el.append(str(ele.value))
|
||||||
|
elif ele.get:
|
||||||
el.append(ele.get())
|
el.append(ele.get())
|
||||||
else:
|
else:
|
||||||
el.append(json.dumps(ele, cls=AlchemyEncoder))
|
el.append(json.dumps(ele, cls=AlchemyEncoder))
|
||||||
@@ -411,12 +421,12 @@ class AlchemyEncoder(json.JSONEncoder):
|
|||||||
else:
|
else:
|
||||||
json.dumps(data)
|
json.dumps(data)
|
||||||
fields[field] = data
|
fields[field] = data
|
||||||
except:
|
except Exception:
|
||||||
fields[field] = ""
|
fields[field] = ""
|
||||||
# a json-encodable dict
|
# a json-encodable dict
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
return json.JSONEncoder.default(self, obj)
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
class CalibreDB():
|
class CalibreDB():
|
||||||
@@ -437,48 +447,13 @@ class CalibreDB():
|
|||||||
|
|
||||||
self.instances.add(self)
|
self.instances.add(self)
|
||||||
|
|
||||||
|
|
||||||
def initSession(self, expire_on_commit=True):
|
def initSession(self, expire_on_commit=True):
|
||||||
self.session = self.session_factory()
|
self.session = self.session_factory()
|
||||||
self.session.expire_on_commit = expire_on_commit
|
self.session.expire_on_commit = expire_on_commit
|
||||||
self.update_title_sort(self.config)
|
self.update_title_sort(self.config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_db(cls, config, app_db_path):
|
def setup_db_cc_classes(self, cc):
|
||||||
cls.config = config
|
|
||||||
cls.dispose()
|
|
||||||
|
|
||||||
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
|
|
||||||
|
|
||||||
if not config.config_calibre_dir:
|
|
||||||
config.invalidate()
|
|
||||||
return False
|
|
||||||
|
|
||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
|
|
||||||
if not os.path.exists(dbpath):
|
|
||||||
config.invalidate()
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
cls.engine = create_engine('sqlite://',
|
|
||||||
echo=False,
|
|
||||||
isolation_level="SERIALIZABLE",
|
|
||||||
connect_args={'check_same_thread': False},
|
|
||||||
poolclass=StaticPool)
|
|
||||||
cls.engine.execute("attach database '{}' as calibre;".format(dbpath))
|
|
||||||
cls.engine.execute("attach database '{}' as app_settings;".format(app_db_path))
|
|
||||||
|
|
||||||
conn = cls.engine.connect()
|
|
||||||
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
|
||||||
except Exception as e:
|
|
||||||
config.invalidate(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
config.db_configured = True
|
|
||||||
|
|
||||||
if not cc_classes:
|
|
||||||
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
|
||||||
|
|
||||||
cc_ids = []
|
cc_ids = []
|
||||||
books_custom_column_links = {}
|
books_custom_column_links = {}
|
||||||
for row in cc:
|
for row in cc:
|
||||||
@@ -498,7 +473,7 @@ class CalibreDB():
|
|||||||
}
|
}
|
||||||
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
|
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
|
||||||
(Base,), dicttable)
|
(Base,), dicttable)
|
||||||
else:
|
if row.datatype in ['rating', 'text', 'enumeration']:
|
||||||
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
|
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
|
||||||
Base.metadata,
|
Base.metadata,
|
||||||
Column('book', Integer, ForeignKey('books.id'),
|
Column('book', Integer, ForeignKey('books.id'),
|
||||||
@@ -516,23 +491,25 @@ class CalibreDB():
|
|||||||
ccdict['value'] = Column(Float)
|
ccdict['value'] = Column(Float)
|
||||||
elif row.datatype == 'int':
|
elif row.datatype == 'int':
|
||||||
ccdict['value'] = Column(Integer)
|
ccdict['value'] = Column(Integer)
|
||||||
|
elif row.datatype == 'datetime':
|
||||||
|
ccdict['value'] = Column(TIMESTAMP)
|
||||||
elif row.datatype == 'bool':
|
elif row.datatype == 'bool':
|
||||||
ccdict['value'] = Column(Boolean)
|
ccdict['value'] = Column(Boolean)
|
||||||
else:
|
else:
|
||||||
ccdict['value'] = Column(String)
|
ccdict['value'] = Column(String)
|
||||||
if row.datatype in ['float', 'int', 'bool']:
|
if row.datatype in ['float', 'int', 'bool', 'datetime', 'comments']:
|
||||||
ccdict['book'] = Column(Integer, ForeignKey('books.id'))
|
ccdict['book'] = Column(Integer, ForeignKey('books.id'))
|
||||||
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
|
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
|
||||||
|
|
||||||
for cc_id in cc_ids:
|
for cc_id in cc_ids:
|
||||||
if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'):
|
if cc_id[1] in ['bool', 'int', 'float', 'datetime', 'comments']:
|
||||||
setattr(Books,
|
setattr(Books,
|
||||||
'custom_column_' + str(cc_id[0]),
|
'custom_column_' + str(cc_id[0]),
|
||||||
relationship(cc_classes[cc_id[0]],
|
relationship(cc_classes[cc_id[0]],
|
||||||
primaryjoin=(
|
primaryjoin=(
|
||||||
Books.id == cc_classes[cc_id[0]].book),
|
Books.id == cc_classes[cc_id[0]].book),
|
||||||
backref='books'))
|
backref='books'))
|
||||||
elif (cc_id[1] == 'series'):
|
elif cc_id[1] == 'series':
|
||||||
setattr(Books,
|
setattr(Books,
|
||||||
'custom_column_' + str(cc_id[0]),
|
'custom_column_' + str(cc_id[0]),
|
||||||
relationship(books_custom_column_links[cc_id[0]],
|
relationship(books_custom_column_links[cc_id[0]],
|
||||||
@@ -544,6 +521,74 @@ class CalibreDB():
|
|||||||
secondary=books_custom_column_links[cc_id[0]],
|
secondary=books_custom_column_links[cc_id[0]],
|
||||||
backref='books'))
|
backref='books'))
|
||||||
|
|
||||||
|
return cc_classes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_valid_db(cls, config_calibre_dir, app_db_path):
|
||||||
|
if not config_calibre_dir:
|
||||||
|
return False
|
||||||
|
dbpath = os.path.join(config_calibre_dir, "metadata.db")
|
||||||
|
if not os.path.exists(dbpath):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
check_engine = create_engine('sqlite://',
|
||||||
|
echo=False,
|
||||||
|
isolation_level="SERIALIZABLE",
|
||||||
|
connect_args={'check_same_thread': False},
|
||||||
|
poolclass=StaticPool)
|
||||||
|
with check_engine.begin() as connection:
|
||||||
|
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
||||||
|
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
||||||
|
check_engine.connect()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_config(cls, config):
|
||||||
|
cls.config = config
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_db(cls, config_calibre_dir, app_db_path):
|
||||||
|
# cls.config = config
|
||||||
|
cls.dispose()
|
||||||
|
|
||||||
|
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
|
||||||
|
|
||||||
|
if not config_calibre_dir:
|
||||||
|
cls.config.invalidate()
|
||||||
|
return False
|
||||||
|
|
||||||
|
dbpath = os.path.join(config_calibre_dir, "metadata.db")
|
||||||
|
if not os.path.exists(dbpath):
|
||||||
|
cls.config.invalidate()
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls.engine = create_engine('sqlite://',
|
||||||
|
echo=False,
|
||||||
|
isolation_level="SERIALIZABLE",
|
||||||
|
connect_args={'check_same_thread': False},
|
||||||
|
poolclass=StaticPool)
|
||||||
|
with cls.engine.begin() as connection:
|
||||||
|
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
||||||
|
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
||||||
|
|
||||||
|
conn = cls.engine.connect()
|
||||||
|
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
||||||
|
except Exception as ex:
|
||||||
|
cls.config.invalidate(ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
cls.config.db_configured = True
|
||||||
|
|
||||||
|
if not cc_classes:
|
||||||
|
try:
|
||||||
|
cc = conn.execute(text("SELECT id, datatype FROM custom_columns"))
|
||||||
|
cls.setup_db_cc_classes(cc)
|
||||||
|
except OperationalError as e:
|
||||||
|
log.debug_or_exception(e)
|
||||||
|
|
||||||
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
|
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
|
||||||
autoflush=True,
|
autoflush=True,
|
||||||
bind=cls.engine))
|
bind=cls.engine))
|
||||||
@@ -563,8 +608,8 @@ class CalibreDB():
|
|||||||
def get_book_by_uuid(self, book_uuid):
|
def get_book_by_uuid(self, book_uuid):
|
||||||
return self.session.query(Books).filter(Books.uuid == book_uuid).first()
|
return self.session.query(Books).filter(Books.uuid == book_uuid).first()
|
||||||
|
|
||||||
def get_book_format(self, book_id, format):
|
def get_book_format(self, book_id, file_format):
|
||||||
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == format).first()
|
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
|
||||||
|
|
||||||
# Language and content filters for displaying in the UI
|
# Language and content filters for displaying in the UI
|
||||||
def common_filters(self, allow_show_archived=False):
|
def common_filters(self, allow_show_archived=False):
|
||||||
@@ -589,6 +634,7 @@ class CalibreDB():
|
|||||||
neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list))
|
neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list))
|
||||||
pos_content_tags_filter = true() if postags_list == [''] else Books.tags.any(Tags.name.in_(postags_list))
|
pos_content_tags_filter = true() if postags_list == [''] else Books.tags.any(Tags.name.in_(postags_list))
|
||||||
if self.config.config_restricted_column:
|
if self.config.config_restricted_column:
|
||||||
|
try:
|
||||||
pos_cc_list = current_user.allowed_column_value.split(',')
|
pos_cc_list = current_user.allowed_column_value.split(',')
|
||||||
pos_content_cc_filter = true() if pos_cc_list == [''] else \
|
pos_content_cc_filter = true() if pos_cc_list == [''] else \
|
||||||
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
||||||
@@ -597,12 +643,37 @@ class CalibreDB():
|
|||||||
neg_content_cc_filter = false() if neg_cc_list == [''] else \
|
neg_content_cc_filter = false() if neg_cc_list == [''] else \
|
||||||
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
||||||
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
|
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
pos_content_cc_filter = false()
|
||||||
|
neg_content_cc_filter = true()
|
||||||
|
log.error(u"Custom Column No.%d is not existing in calibre database",
|
||||||
|
self.config.config_restricted_column)
|
||||||
|
flash(_("Custom Column No.%(column)d is not existing in calibre database",
|
||||||
|
column=self.config.config_restricted_column),
|
||||||
|
category="error")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pos_content_cc_filter = true()
|
pos_content_cc_filter = true()
|
||||||
neg_content_cc_filter = false()
|
neg_content_cc_filter = false()
|
||||||
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
|
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
|
||||||
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
|
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_checkbox_sorted(inputlist, state, offset, limit, order):
|
||||||
|
outcome = list()
|
||||||
|
elementlist = {ele.id: ele for ele in inputlist}
|
||||||
|
for entry in state:
|
||||||
|
try:
|
||||||
|
outcome.append(elementlist[entry])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
del elementlist[entry]
|
||||||
|
for entry in elementlist:
|
||||||
|
outcome.append(elementlist[entry])
|
||||||
|
if order == "asc":
|
||||||
|
outcome.reverse()
|
||||||
|
return outcome[offset:offset + limit]
|
||||||
|
|
||||||
# Fill indexpage with all requested data from database
|
# Fill indexpage with all requested data from database
|
||||||
def fill_indexpage(self, page, pagesize, database, db_filter, order, *join):
|
def fill_indexpage(self, page, pagesize, database, db_filter, order, *join):
|
||||||
return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
|
return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
|
||||||
@@ -614,22 +685,35 @@ class CalibreDB():
|
|||||||
randm = self.session.query(Books) \
|
randm = self.session.query(Books) \
|
||||||
.filter(self.common_filters(allow_show_archived)) \
|
.filter(self.common_filters(allow_show_archived)) \
|
||||||
.order_by(func.random()) \
|
.order_by(func.random()) \
|
||||||
.limit(self.config.config_random_books)
|
.limit(self.config.config_random_books).all()
|
||||||
else:
|
else:
|
||||||
randm = false()
|
randm = false()
|
||||||
off = int(int(pagesize) * (page - 1))
|
off = int(int(pagesize) * (page - 1))
|
||||||
query = self.session.query(database) \
|
query = self.session.query(database)
|
||||||
.join(*join, isouter=True) \
|
if len(join) == 6:
|
||||||
.filter(db_filter) \
|
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
|
||||||
|
if len(join) == 5:
|
||||||
|
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4])
|
||||||
|
if len(join) == 4:
|
||||||
|
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3])
|
||||||
|
if len(join) == 3:
|
||||||
|
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
|
||||||
|
elif len(join) == 2:
|
||||||
|
query = query.outerjoin(join[0], join[1])
|
||||||
|
elif len(join) == 1:
|
||||||
|
query = query.outerjoin(join[0])
|
||||||
|
query = query.filter(db_filter)\
|
||||||
.filter(self.common_filters(allow_show_archived))
|
.filter(self.common_filters(allow_show_archived))
|
||||||
|
entries = list()
|
||||||
|
pagination = list()
|
||||||
try:
|
try:
|
||||||
pagination = Pagination(page, pagesize,
|
pagination = Pagination(page, pagesize,
|
||||||
len(query.all()))
|
len(query.all()))
|
||||||
entries = query.order_by(*order).offset(off).limit(pagesize).all()
|
entries = query.order_by(*order).offset(off).limit(pagesize).all()
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
logger.debug(e)
|
log.debug_or_exception(ex)
|
||||||
for book in entries:
|
#for book in entries:
|
||||||
book = self.order_authors(book)
|
# book = self.order_authors(book)
|
||||||
return entries, randm, pagination
|
return entries, randm, pagination
|
||||||
|
|
||||||
# Orders all Authors in the list according to authors sort
|
# Orders all Authors in the list according to authors sort
|
||||||
@@ -656,6 +740,7 @@ class CalibreDB():
|
|||||||
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
entries = self.session.query(database).filter(tag_filter). \
|
entries = self.session.query(database).filter(tag_filter). \
|
||||||
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
||||||
|
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
|
||||||
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
||||||
return json_dumps
|
return json_dumps
|
||||||
|
|
||||||
@@ -669,23 +754,35 @@ class CalibreDB():
|
|||||||
return self.session.query(Books) \
|
return self.session.query(Books) \
|
||||||
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
|
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
|
||||||
|
|
||||||
# read search results from calibre-database and return it (function is used for feed and simple search
|
def search_query(self, term, *join):
|
||||||
def get_search_results(self, term, offset=None, order=None, limit=None):
|
|
||||||
order = order or [Books.sort]
|
|
||||||
pagination = None
|
|
||||||
term.strip().lower()
|
term.strip().lower()
|
||||||
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
q = list()
|
q = list()
|
||||||
authorterms = re.split("[, ]+", term)
|
authorterms = re.split("[, ]+", term)
|
||||||
for authorterm in authorterms:
|
for authorterm in authorterms:
|
||||||
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
|
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
|
||||||
result = self.session.query(Books).filter(self.common_filters(True)).filter(
|
query = self.session.query(Books)
|
||||||
|
if len(join) == 6:
|
||||||
|
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
|
||||||
|
if len(join) == 3:
|
||||||
|
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
|
||||||
|
elif len(join) == 2:
|
||||||
|
query = query.outerjoin(join[0], join[1])
|
||||||
|
elif len(join) == 1:
|
||||||
|
query = query.outerjoin(join[0])
|
||||||
|
return query.filter(self.common_filters(True)).filter(
|
||||||
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
|
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
|
||||||
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
|
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
|
||||||
Books.authors.any(and_(*q)),
|
Books.authors.any(and_(*q)),
|
||||||
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
|
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
|
||||||
func.lower(Books.title).ilike("%" + term + "%")
|
func.lower(Books.title).ilike("%" + term + "%")
|
||||||
)).order_by(*order).all()
|
))
|
||||||
|
|
||||||
|
# read search results from calibre-database and return it (function is used for feed and simple search
|
||||||
|
def get_search_results(self, term, offset=None, order=None, limit=None, *join):
|
||||||
|
order = order or [Books.sort]
|
||||||
|
pagination = None
|
||||||
|
result = self.search_query(term, *join).order_by(*order).all()
|
||||||
result_count = len(result)
|
result_count = len(result)
|
||||||
if offset != None and limit != None:
|
if offset != None and limit != None:
|
||||||
offset = int(offset)
|
offset = int(offset)
|
||||||
@@ -709,11 +806,16 @@ class CalibreDB():
|
|||||||
.filter(self.common_filters()) \
|
.filter(self.common_filters()) \
|
||||||
.group_by(text('books_languages_link.lang_code')).all()
|
.group_by(text('books_languages_link.lang_code')).all()
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
try:
|
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||||
cur_l = LC.parse(lang.lang_code)
|
#try:
|
||||||
lang.name = cur_l.get_language_name(get_locale())
|
# if lang.lang_code.lower() == "und":
|
||||||
except UnknownLocaleError:
|
# lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||||
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
|
# # lang.name = _("Undetermined")
|
||||||
|
# else:
|
||||||
|
# cur_l = LC.parse(lang.lang_code)
|
||||||
|
# lang.name = cur_l.get_language_name(get_locale())
|
||||||
|
#except UnknownLocaleError:
|
||||||
|
# lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
|
||||||
return languages
|
return languages
|
||||||
|
|
||||||
def update_title_sort(self, config, conn=None):
|
def update_title_sort(self, config, conn=None):
|
||||||
@@ -740,7 +842,7 @@ class CalibreDB():
|
|||||||
if old_session:
|
if old_session:
|
||||||
try:
|
try:
|
||||||
old_session.close()
|
old_session.close()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if old_session.bind:
|
if old_session.bind:
|
||||||
try:
|
try:
|
||||||
@@ -765,13 +867,14 @@ class CalibreDB():
|
|||||||
def reconnect_db(self, config, app_db_path):
|
def reconnect_db(self, config, app_db_path):
|
||||||
self.dispose()
|
self.dispose()
|
||||||
self.engine.dispose()
|
self.engine.dispose()
|
||||||
self.setup_db(config, app_db_path)
|
self.setup_db(config.config_calibre_dir, app_db_path)
|
||||||
|
self.update_config(config)
|
||||||
|
|
||||||
|
|
||||||
def lcase(s):
|
def lcase(s):
|
||||||
try:
|
try:
|
||||||
return unidecode.unidecode(s.lower())
|
return unidecode.unidecode(s.lower())
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
return s.lower()
|
return s.lower()
|
||||||
|
|||||||
+14
-7
@@ -22,14 +22,10 @@ import glob
|
|||||||
import zipfile
|
import zipfile
|
||||||
import json
|
import json
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
try:
|
|
||||||
from StringIO import StringIO
|
|
||||||
except ImportError:
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import send_file
|
from flask import send_file, __version__
|
||||||
|
|
||||||
from . import logger, config
|
from . import logger, config
|
||||||
from .about import collect_stats
|
from .about import collect_stats
|
||||||
@@ -38,14 +34,20 @@ log = logger.create()
|
|||||||
|
|
||||||
def assemble_logfiles(file_name):
|
def assemble_logfiles(file_name):
|
||||||
log_list = sorted(glob.glob(file_name + '*'), reverse=True)
|
log_list = sorted(glob.glob(file_name + '*'), reverse=True)
|
||||||
wfd = StringIO()
|
wfd = BytesIO()
|
||||||
for f in log_list:
|
for f in log_list:
|
||||||
with open(f, 'r') as fd:
|
with open(f, 'rb') as fd:
|
||||||
shutil.copyfileobj(fd, wfd)
|
shutil.copyfileobj(fd, wfd)
|
||||||
wfd.seek(0)
|
wfd.seek(0)
|
||||||
|
if int(__version__.split('.')[0]) < 2:
|
||||||
return send_file(wfd,
|
return send_file(wfd,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename=os.path.basename(file_name))
|
attachment_filename=os.path.basename(file_name))
|
||||||
|
else:
|
||||||
|
return send_file(wfd,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=os.path.basename(file_name))
|
||||||
|
|
||||||
|
|
||||||
def send_debug():
|
def send_debug():
|
||||||
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
|
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
|
||||||
@@ -60,6 +62,11 @@ def send_debug():
|
|||||||
for fp in file_list:
|
for fp in file_list:
|
||||||
zf.write(fp, os.path.basename(fp))
|
zf.write(fp, os.path.basename(fp))
|
||||||
memory_zip.seek(0)
|
memory_zip.seek(0)
|
||||||
|
if int(__version__.split('.')[0]) < 2:
|
||||||
return send_file(memory_zip,
|
return send_file(memory_zip,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename="Calibre-Web-debug-pack.zip")
|
attachment_filename="Calibre-Web-debug-pack.zip")
|
||||||
|
else:
|
||||||
|
return send_file(memory_zip,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name="Calibre-Web-debug-pack.zip")
|
||||||
|
|||||||
+418
-198
@@ -20,19 +20,31 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from markupsafe import escape
|
||||||
|
try:
|
||||||
|
from lxml.html.clean import clean_html
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Improve this to check if scholarly is available in a global way, like other pythonic libraries
|
||||||
|
try:
|
||||||
|
from scholarly import scholarly
|
||||||
|
have_scholar = True
|
||||||
|
except ImportError:
|
||||||
|
have_scholar = False
|
||||||
|
|
||||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from sqlalchemy.exc import OperationalError, IntegrityError
|
from sqlalchemy.exc import OperationalError, IntegrityError
|
||||||
from sqlite3 import OperationalError as sqliteOperationalError
|
from sqlite3 import OperationalError as sqliteOperationalError
|
||||||
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper
|
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
|
||||||
from . import config, get_locale, ub, db
|
from . import config, get_locale, ub, db
|
||||||
from . import calibre_db
|
from . import calibre_db
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
@@ -46,6 +58,8 @@ except ImportError:
|
|||||||
pass # We're not using Python 3
|
pass # We're not using Python 3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
editbook = Blueprint('editbook', __name__)
|
editbook = Blueprint('editbook', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -68,17 +82,7 @@ def edit_required(f):
|
|||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
def search_objects_remove(db_book_object, db_type, input_elements):
|
||||||
# Modifies different Database objects, first check if elements have to be added to database, than check
|
|
||||||
# if elements have to be deleted, because they are no longer used
|
|
||||||
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
|
|
||||||
# passing input_elements not as a list may lead to undesired results
|
|
||||||
if not isinstance(input_elements, list):
|
|
||||||
raise TypeError(str(input_elements) + " should be passed as a list")
|
|
||||||
changed = False
|
|
||||||
input_elements = [x for x in input_elements if x != '']
|
|
||||||
# we have all input element (authors, series, tags) names now
|
|
||||||
# 1. search for elements to remove
|
|
||||||
del_elements = []
|
del_elements = []
|
||||||
for c_elements in db_book_object:
|
for c_elements in db_book_object:
|
||||||
found = False
|
found = False
|
||||||
@@ -96,7 +100,10 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
# if the element was not found in the new list, add it to remove list
|
# if the element was not found in the new list, add it to remove list
|
||||||
if not found:
|
if not found:
|
||||||
del_elements.append(c_elements)
|
del_elements.append(c_elements)
|
||||||
# 2. search for elements that need to be added
|
return del_elements
|
||||||
|
|
||||||
|
|
||||||
|
def search_objects_add(db_book_object, db_type, input_elements):
|
||||||
add_elements = []
|
add_elements = []
|
||||||
for inp_element in input_elements:
|
for inp_element in input_elements:
|
||||||
found = False
|
found = False
|
||||||
@@ -112,15 +119,21 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
add_elements.append(inp_element)
|
add_elements.append(inp_element)
|
||||||
# if there are elements to remove, we remove them now
|
return add_elements
|
||||||
|
|
||||||
|
|
||||||
|
def remove_objects(db_book_object, db_session, del_elements):
|
||||||
|
changed = False
|
||||||
if len(del_elements) > 0:
|
if len(del_elements) > 0:
|
||||||
for del_element in del_elements:
|
for del_element in del_elements:
|
||||||
db_book_object.remove(del_element)
|
db_book_object.remove(del_element)
|
||||||
changed = True
|
changed = True
|
||||||
if len(del_element.books) == 0:
|
if len(del_element.books) == 0:
|
||||||
db_session.delete(del_element)
|
db_session.delete(del_element)
|
||||||
# if there are elements to add, we add them now!
|
return changed
|
||||||
if len(add_elements) > 0:
|
|
||||||
|
def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
|
||||||
|
changed = False
|
||||||
if db_type == 'languages':
|
if db_type == 'languages':
|
||||||
db_filter = db_object.lang_code
|
db_filter = db_object.lang_code
|
||||||
elif db_type == 'custom':
|
elif db_type == 'custom':
|
||||||
@@ -147,9 +160,18 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
db_session.add(new_element)
|
db_session.add(new_element)
|
||||||
db_book_object.append(new_element)
|
db_book_object.append(new_element)
|
||||||
else:
|
else:
|
||||||
|
db_element = create_objects_for_addition(db_element, add_element, db_type)
|
||||||
|
changed = True
|
||||||
|
# add element to book
|
||||||
|
changed = True
|
||||||
|
db_book_object.append(db_element)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def create_objects_for_addition(db_element, add_element, db_type):
|
||||||
if db_type == 'custom':
|
if db_type == 'custom':
|
||||||
if db_element.value != add_element:
|
if db_element.value != add_element:
|
||||||
new_element.value = add_element
|
db_element.value = add_element # ToDo: Before new_element, but this is not plausible
|
||||||
elif db_type == 'languages':
|
elif db_type == 'languages':
|
||||||
if db_element.lang_code != add_element:
|
if db_element.lang_code != add_element:
|
||||||
db_element.lang_code = add_element
|
db_element.lang_code = add_element
|
||||||
@@ -167,9 +189,26 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
db_element.sort = None
|
db_element.sort = None
|
||||||
elif db_element.name != add_element:
|
elif db_element.name != add_element:
|
||||||
db_element.name = add_element
|
db_element.name = add_element
|
||||||
# add element to book
|
return db_element
|
||||||
changed = True
|
|
||||||
db_book_object.append(db_element)
|
|
||||||
|
# Modifies different Database objects, first check if elements if elements have to be deleted,
|
||||||
|
# because they are no longer used, than check if elements have to be added to database
|
||||||
|
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
|
||||||
|
# passing input_elements not as a list may lead to undesired results
|
||||||
|
if not isinstance(input_elements, list):
|
||||||
|
raise TypeError(str(input_elements) + " should be passed as a list")
|
||||||
|
input_elements = [x for x in input_elements if x != '']
|
||||||
|
# we have all input element (authors, series, tags) names now
|
||||||
|
# 1. search for elements to remove
|
||||||
|
del_elements = search_objects_remove(db_book_object, db_type, input_elements)
|
||||||
|
# 2. search for elements that need to be added
|
||||||
|
add_elements = search_objects_add(db_book_object, db_type, input_elements)
|
||||||
|
# if there are elements to remove, we remove them now
|
||||||
|
changed = remove_objects(db_book_object, db_session, del_elements)
|
||||||
|
# if there are elements to add, we add them now!
|
||||||
|
if len(add_elements) > 0:
|
||||||
|
changed |= add_objects(db_book_object, db_object, db_session, db_type, add_elements)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
@@ -211,31 +250,8 @@ def delete_book_from_details(book_id):
|
|||||||
def delete_book_ajax(book_id, book_format):
|
def delete_book_ajax(book_id, book_format):
|
||||||
return delete_book(book_id, book_format, False)
|
return delete_book(book_id, book_format, False)
|
||||||
|
|
||||||
def delete_book(book_id, book_format, jsonResponse):
|
|
||||||
warning = {}
|
def delete_whole_book(book_id, book):
|
||||||
if current_user.role_delete_books():
|
|
||||||
book = calibre_db.get_book(book_id)
|
|
||||||
if book:
|
|
||||||
try:
|
|
||||||
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
|
|
||||||
if not result:
|
|
||||||
if jsonResponse:
|
|
||||||
return json.dumps({"location": url_for("editbook.edit_book"),
|
|
||||||
"type": "alert",
|
|
||||||
"format": "",
|
|
||||||
"error": error}),
|
|
||||||
else:
|
|
||||||
flash(error, category="error")
|
|
||||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
|
||||||
if error:
|
|
||||||
if jsonResponse:
|
|
||||||
warning = {"location": url_for("editbook.edit_book"),
|
|
||||||
"type": "warning",
|
|
||||||
"format": "",
|
|
||||||
"error": error}
|
|
||||||
else:
|
|
||||||
flash(error, category="warning")
|
|
||||||
if not book_format:
|
|
||||||
# delete book from Shelfs, Downloads, Read list
|
# delete book from Shelfs, Downloads, Read list
|
||||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
|
||||||
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
|
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
|
||||||
@@ -279,16 +295,9 @@ def delete_book(book_id, book_format, jsonResponse):
|
|||||||
modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
|
modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
|
||||||
calibre_db.session, 'custom')
|
calibre_db.session, 'custom')
|
||||||
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
||||||
else:
|
|
||||||
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
|
|
||||||
filter(db.Data.format == book_format).delete()
|
def render_delete_book_result(book_format, jsonResponse, warning, book_id):
|
||||||
calibre_db.session.commit()
|
|
||||||
except Exception as e:
|
|
||||||
log.debug_or_exception(e)
|
|
||||||
calibre_db.session.rollback()
|
|
||||||
else:
|
|
||||||
# book not found
|
|
||||||
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
|
||||||
if book_format:
|
if book_format:
|
||||||
if jsonResponse:
|
if jsonResponse:
|
||||||
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
|
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
|
||||||
@@ -309,11 +318,59 @@ def delete_book(book_id, book_format, jsonResponse):
|
|||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_book(book_id, book_format, jsonResponse):
|
||||||
|
warning = {}
|
||||||
|
if current_user.role_delete_books():
|
||||||
|
book = calibre_db.get_book(book_id)
|
||||||
|
if book:
|
||||||
|
try:
|
||||||
|
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
|
||||||
|
if not result:
|
||||||
|
if jsonResponse:
|
||||||
|
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id),
|
||||||
|
"type": "danger",
|
||||||
|
"format": "",
|
||||||
|
"message": error}])
|
||||||
|
else:
|
||||||
|
flash(error, category="error")
|
||||||
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
|
if error:
|
||||||
|
if jsonResponse:
|
||||||
|
warning = {"location": url_for("editbook.edit_book", book_id=book_id),
|
||||||
|
"type": "warning",
|
||||||
|
"format": "",
|
||||||
|
"message": error}
|
||||||
|
else:
|
||||||
|
flash(error, category="warning")
|
||||||
|
if not book_format:
|
||||||
|
delete_whole_book(book_id, book)
|
||||||
|
else:
|
||||||
|
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
|
||||||
|
filter(db.Data.format == book_format).delete()
|
||||||
|
calibre_db.session.commit()
|
||||||
|
except Exception as ex:
|
||||||
|
log.debug_or_exception(ex)
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
if jsonResponse:
|
||||||
|
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id),
|
||||||
|
"type": "danger",
|
||||||
|
"format": "",
|
||||||
|
"message": ex}])
|
||||||
|
else:
|
||||||
|
flash(str(ex), category="error")
|
||||||
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# book not found
|
||||||
|
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
||||||
|
return render_delete_book_result(book_format, jsonResponse, warning, book_id)
|
||||||
|
|
||||||
|
|
||||||
def render_edit_book(book_id):
|
def render_edit_book(book_id):
|
||||||
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
if not book:
|
if not book:
|
||||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
for lang in book.languages:
|
for lang in book.languages:
|
||||||
@@ -394,14 +451,20 @@ def edit_book_series_index(series_index, book):
|
|||||||
# Add default series_index to book
|
# Add default series_index to book
|
||||||
modif_date = False
|
modif_date = False
|
||||||
series_index = series_index or '1'
|
series_index = series_index or '1'
|
||||||
|
if not series_index.replace('.', '', 1).isdigit():
|
||||||
|
flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
|
||||||
|
return False
|
||||||
if book.series_index != series_index:
|
if book.series_index != series_index:
|
||||||
book.series_index = series_index
|
book.series_index = series_index
|
||||||
modif_date = True
|
modif_date = True
|
||||||
return modif_date
|
return modif_date
|
||||||
|
|
||||||
|
|
||||||
# Handle book comments/description
|
# Handle book comments/description
|
||||||
def edit_book_comments(comments, book):
|
def edit_book_comments(comments, book):
|
||||||
modif_date = False
|
modif_date = False
|
||||||
|
if comments:
|
||||||
|
comments = clean_html(comments)
|
||||||
if len(book.comments):
|
if len(book.comments):
|
||||||
if book.comments[0].text != comments:
|
if book.comments[0].text != comments:
|
||||||
book.comments[0].text = comments
|
book.comments[0].text = comments
|
||||||
@@ -413,7 +476,7 @@ def edit_book_comments(comments, book):
|
|||||||
return modif_date
|
return modif_date
|
||||||
|
|
||||||
|
|
||||||
def edit_book_languages(languages, book, upload=False):
|
def edit_book_languages(languages, book, upload=False, invalid=None):
|
||||||
input_languages = languages.split(',')
|
input_languages = languages.split(',')
|
||||||
unknown_languages = []
|
unknown_languages = []
|
||||||
if not upload:
|
if not upload:
|
||||||
@@ -422,6 +485,9 @@ def edit_book_languages(languages, book, upload=False):
|
|||||||
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
|
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
|
||||||
for l in unknown_languages:
|
for l in unknown_languages:
|
||||||
log.error('%s is not a valid language', l)
|
log.error('%s is not a valid language', l)
|
||||||
|
if isinstance(invalid, list):
|
||||||
|
invalid.append(l)
|
||||||
|
else:
|
||||||
flash(_(u"%(langname)s is not a valid language", langname=l), category="warning")
|
flash(_(u"%(langname)s is not a valid language", langname=l), category="warning")
|
||||||
# ToDo: Not working correct
|
# ToDo: Not working correct
|
||||||
if upload and len(input_l) == 1:
|
if upload and len(input_l) == 1:
|
||||||
@@ -435,10 +501,10 @@ def edit_book_languages(languages, book, upload=False):
|
|||||||
return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages')
|
return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages')
|
||||||
|
|
||||||
|
|
||||||
def edit_book_publisher(to_save, book):
|
def edit_book_publisher(publishers, book):
|
||||||
changed = False
|
changed = False
|
||||||
if to_save["publisher"]:
|
if publishers:
|
||||||
publisher = to_save["publisher"].rstrip().strip()
|
publisher = publishers.rstrip().strip()
|
||||||
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
|
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
|
||||||
changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session,
|
changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session,
|
||||||
'publisher')
|
'publisher')
|
||||||
@@ -447,22 +513,21 @@ def edit_book_publisher(to_save, book):
|
|||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def edit_cc_data(book_id, book, to_save):
|
def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string):
|
||||||
changed = False
|
changed = False
|
||||||
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
|
||||||
for c in cc:
|
|
||||||
cc_string = "custom_column_" + str(c.id)
|
|
||||||
if not c.is_multiple:
|
|
||||||
if len(getattr(book, cc_string)) > 0:
|
|
||||||
cc_db_value = getattr(book, cc_string)[0].value
|
|
||||||
else:
|
|
||||||
cc_db_value = None
|
|
||||||
if to_save[cc_string].strip():
|
|
||||||
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float':
|
|
||||||
if to_save[cc_string] == 'None':
|
if to_save[cc_string] == 'None':
|
||||||
to_save[cc_string] = None
|
to_save[cc_string] = None
|
||||||
elif c.datatype == 'bool':
|
elif c.datatype == 'bool':
|
||||||
to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
|
to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
|
||||||
|
elif c.datatype == 'comments':
|
||||||
|
to_save[cc_string] = Markup(to_save[cc_string]).unescape()
|
||||||
|
if to_save[cc_string]:
|
||||||
|
to_save[cc_string] = clean_html(to_save[cc_string])
|
||||||
|
elif c.datatype == 'datetime':
|
||||||
|
try:
|
||||||
|
to_save[cc_string] = datetime.strptime(to_save[cc_string], "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
to_save[cc_string] = db.Books.DEFAULT_PUBDATE
|
||||||
|
|
||||||
if to_save[cc_string] != cc_db_value:
|
if to_save[cc_string] != cc_db_value:
|
||||||
if cc_db_value is not None:
|
if cc_db_value is not None:
|
||||||
@@ -479,8 +544,11 @@ def edit_cc_data(book_id, book, to_save):
|
|||||||
new_cc = cc_class(value=to_save[cc_string], book=book_id)
|
new_cc = cc_class(value=to_save[cc_string], book=book_id)
|
||||||
calibre_db.session.add(new_cc)
|
calibre_db.session.add(new_cc)
|
||||||
changed = True
|
changed = True
|
||||||
|
return changed, to_save
|
||||||
|
|
||||||
else:
|
|
||||||
|
def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
|
||||||
|
changed = False
|
||||||
if c.datatype == 'rating':
|
if c.datatype == 'rating':
|
||||||
to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
|
to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
|
||||||
if to_save[cc_string].strip() != cc_db_value:
|
if to_save[cc_string].strip() != cc_db_value:
|
||||||
@@ -504,6 +572,33 @@ def edit_cc_data(book_id, book, to_save):
|
|||||||
cc_class.value == to_save[cc_string].strip()).first()
|
cc_class.value == to_save[cc_string].strip()).first()
|
||||||
# add cc value to book
|
# add cc value to book
|
||||||
getattr(book, cc_string).append(new_cc)
|
getattr(book, cc_string).append(new_cc)
|
||||||
|
return changed, to_save
|
||||||
|
|
||||||
|
def edit_single_cc_data(book_id, book, column_id, to_save):
|
||||||
|
cc = (calibre_db.session.query(db.Custom_Columns)
|
||||||
|
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions))
|
||||||
|
.filter(db.Custom_Columns.id == column_id)
|
||||||
|
.all())
|
||||||
|
return edit_cc_data(book_id, book, to_save, cc)
|
||||||
|
|
||||||
|
def edit_all_cc_data(book_id, book, to_save):
|
||||||
|
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
|
return edit_cc_data(book_id, book, to_save, cc)
|
||||||
|
|
||||||
|
def edit_cc_data(book_id, book, to_save, cc):
|
||||||
|
changed = False
|
||||||
|
for c in cc:
|
||||||
|
cc_string = "custom_column_" + str(c.id)
|
||||||
|
if not c.is_multiple:
|
||||||
|
if len(getattr(book, cc_string)) > 0:
|
||||||
|
cc_db_value = getattr(book, cc_string)[0].value
|
||||||
|
else:
|
||||||
|
cc_db_value = None
|
||||||
|
if to_save[cc_string].strip():
|
||||||
|
if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]:
|
||||||
|
changed, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
|
||||||
|
else:
|
||||||
|
changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
|
||||||
else:
|
else:
|
||||||
if cc_db_value is not None:
|
if cc_db_value is not None:
|
||||||
# remove old cc_val
|
# remove old cc_val
|
||||||
@@ -576,9 +671,9 @@ def upload_single_file(request, book, book_id):
|
|||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
|
|
||||||
# Queue uploader info
|
# Queue uploader info
|
||||||
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
|
||||||
WorkerThread.add(current_user.nickname, TaskUpload(
|
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
|
||||||
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>"))
|
WorkerThread.add(current_user.name, TaskUpload(uploadText))
|
||||||
|
|
||||||
return uploader.process(
|
return uploader.process(
|
||||||
saved_filename, *os.path.splitext(requested_file.filename),
|
saved_filename, *os.path.splitext(requested_file.filename),
|
||||||
@@ -601,6 +696,46 @@ def upload_cover(request, book):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def handle_title_on_edit(book, book_title):
|
||||||
|
# handle book title
|
||||||
|
book_title = book_title.rstrip().strip()
|
||||||
|
if book.title != book_title:
|
||||||
|
if book_title == '':
|
||||||
|
book_title = _(u'Unknown')
|
||||||
|
book.title = book_title
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def handle_author_on_edit(book, author_name, update_stored=True):
|
||||||
|
# handle author(s)
|
||||||
|
input_authors = author_name.split('&')
|
||||||
|
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
|
||||||
|
# Remove duplicates in authors list
|
||||||
|
input_authors = helper.uniq(input_authors)
|
||||||
|
# we have all author names now
|
||||||
|
if input_authors == ['']:
|
||||||
|
input_authors = [_(u'Unknown')] # prevent empty Author
|
||||||
|
|
||||||
|
change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
||||||
|
|
||||||
|
# Search for each author if author is in database, if not, author name and sorted author name is generated new
|
||||||
|
# everything then is assembled for sorted author field in database
|
||||||
|
sort_authors_list = list()
|
||||||
|
for inp in input_authors:
|
||||||
|
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
||||||
|
if not stored_author:
|
||||||
|
stored_author = helper.get_sorted_author(inp)
|
||||||
|
else:
|
||||||
|
stored_author = stored_author.sort
|
||||||
|
sort_authors_list.append(helper.get_sorted_author(stored_author))
|
||||||
|
sort_authors = ' & '.join(sort_authors_list)
|
||||||
|
if book.author_sort != sort_authors and update_stored:
|
||||||
|
book.author_sort = sort_authors
|
||||||
|
change = True
|
||||||
|
return input_authors, change
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
|
@editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
@@ -618,12 +753,11 @@ def edit_book(book_id):
|
|||||||
if request.method != 'POST':
|
if request.method != 'POST':
|
||||||
return render_edit_book(book_id)
|
return render_edit_book(book_id)
|
||||||
|
|
||||||
|
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
|
|
||||||
# Book not found
|
# Book not found
|
||||||
if not book:
|
if not book:
|
||||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
meta = upload_single_file(request, book, book_id)
|
meta = upload_single_file(request, book, book_id)
|
||||||
@@ -637,40 +771,13 @@ def edit_book(book_id):
|
|||||||
edited_books_id = None
|
edited_books_id = None
|
||||||
|
|
||||||
# handle book title
|
# handle book title
|
||||||
if book.title != to_save["book_title"].rstrip().strip():
|
title_change = handle_title_on_edit(book, to_save["book_title"])
|
||||||
if to_save["book_title"] == '':
|
|
||||||
to_save["book_title"] = _(u'Unknown')
|
input_authors, authorchange = handle_author_on_edit(book, to_save["author_name"])
|
||||||
book.title = to_save["book_title"].rstrip().strip()
|
if authorchange or title_change:
|
||||||
edited_books_id = book.id
|
edited_books_id = book.id
|
||||||
modif_date = True
|
modif_date = True
|
||||||
|
|
||||||
# handle author(s)
|
|
||||||
input_authors = to_save["author_name"].split('&')
|
|
||||||
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
|
|
||||||
# Remove duplicates in authors list
|
|
||||||
input_authors = helper.uniq(input_authors)
|
|
||||||
# we have all author names now
|
|
||||||
if input_authors == ['']:
|
|
||||||
input_authors = [_(u'Unknown')] # prevent empty Author
|
|
||||||
|
|
||||||
modif_date |= modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
|
||||||
|
|
||||||
# Search for each author if author is in database, if not, authorname and sorted authorname is generated new
|
|
||||||
# everything then is assembled for sorted author field in database
|
|
||||||
sort_authors_list = list()
|
|
||||||
for inp in input_authors:
|
|
||||||
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
|
||||||
if not stored_author:
|
|
||||||
stored_author = helper.get_sorted_author(inp)
|
|
||||||
else:
|
|
||||||
stored_author = stored_author.sort
|
|
||||||
sort_authors_list.append(helper.get_sorted_author(stored_author))
|
|
||||||
sort_authors = ' & '.join(sort_authors_list)
|
|
||||||
if book.author_sort != sort_authors:
|
|
||||||
edited_books_id = book.id
|
|
||||||
book.author_sort = sort_authors
|
|
||||||
modif_date = True
|
|
||||||
|
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
|
|
||||||
@@ -695,10 +802,8 @@ def edit_book(book_id):
|
|||||||
|
|
||||||
# Add default series_index to book
|
# Add default series_index to book
|
||||||
modif_date |= edit_book_series_index(to_save["series_index"], book)
|
modif_date |= edit_book_series_index(to_save["series_index"], book)
|
||||||
|
|
||||||
# Handle book comments/description
|
# Handle book comments/description
|
||||||
modif_date |= edit_book_comments(to_save["description"], book)
|
modif_date |= edit_book_comments(Markup(to_save['description']).unescape(), book)
|
||||||
|
|
||||||
# Handle identifiers
|
# Handle identifiers
|
||||||
input_identifiers = identifier_list(to_save, book)
|
input_identifiers = identifier_list(to_save, book)
|
||||||
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
|
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
|
||||||
@@ -707,9 +812,16 @@ def edit_book(book_id):
|
|||||||
modif_date |= modification
|
modif_date |= modification
|
||||||
# Handle book tags
|
# Handle book tags
|
||||||
modif_date |= edit_book_tags(to_save['tags'], book)
|
modif_date |= edit_book_tags(to_save['tags'], book)
|
||||||
|
|
||||||
# Handle book series
|
# Handle book series
|
||||||
modif_date |= edit_book_series(to_save["series"], book)
|
modif_date |= edit_book_series(to_save["series"], book)
|
||||||
|
# handle book publisher
|
||||||
|
modif_date |= edit_book_publisher(to_save['publisher'], book)
|
||||||
|
# handle book languages
|
||||||
|
modif_date |= edit_book_languages(to_save['languages'], book)
|
||||||
|
# handle book ratings
|
||||||
|
modif_date |= edit_book_ratings(to_save, book)
|
||||||
|
# handle cc data
|
||||||
|
modif_date |= edit_all_cc_data(book_id, book, to_save)
|
||||||
|
|
||||||
if to_save["pubdate"]:
|
if to_save["pubdate"]:
|
||||||
try:
|
try:
|
||||||
@@ -719,20 +831,10 @@ def edit_book(book_id):
|
|||||||
else:
|
else:
|
||||||
book.pubdate = db.Books.DEFAULT_PUBDATE
|
book.pubdate = db.Books.DEFAULT_PUBDATE
|
||||||
|
|
||||||
# handle book publisher
|
|
||||||
modif_date |= edit_book_publisher(to_save, book)
|
|
||||||
|
|
||||||
# handle book languages
|
|
||||||
modif_date |= edit_book_languages(to_save['languages'], book)
|
|
||||||
|
|
||||||
# handle book ratings
|
|
||||||
modif_date |= edit_book_ratings(to_save, book)
|
|
||||||
|
|
||||||
# handle cc data
|
|
||||||
modif_date |= edit_cc_data(book_id, book, to_save)
|
|
||||||
|
|
||||||
if modif_date:
|
if modif_date:
|
||||||
book.last_modified = datetime.utcnow()
|
book.last_modified = datetime.utcnow()
|
||||||
|
kobo_sync_status.remove_synced_book(edited_books_id)
|
||||||
|
|
||||||
calibre_db.session.merge(book)
|
calibre_db.session.merge(book)
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
@@ -746,8 +848,8 @@ def edit_book(book_id):
|
|||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
return render_edit_book(book_id)
|
return render_edit_book(book_id)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
flash(_("Error editing book, please check logfile for details"), category="error")
|
flash(_("Error editing book, please check logfile for details"), category="error")
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
@@ -766,6 +868,7 @@ def merge_metadata(to_save, meta):
|
|||||||
to_save["description"] = to_save["description"] or Markup(
|
to_save["description"] = to_save["description"] or Markup(
|
||||||
getattr(meta, 'description', '')).unescape()
|
getattr(meta, 'description', '')).unescape()
|
||||||
|
|
||||||
|
|
||||||
def identifier_list(to_save, book):
|
def identifier_list(to_save, book):
|
||||||
"""Generate a list of Identifiers from form information"""
|
"""Generate a list of Identifiers from form information"""
|
||||||
id_type_prefix = 'identifier-type-'
|
id_type_prefix = 'identifier-type-'
|
||||||
@@ -780,43 +883,8 @@ def identifier_list(to_save, book):
|
|||||||
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
|
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@editbook.route("/upload", methods=["GET", "POST"])
|
|
||||||
@login_required_if_no_ano
|
|
||||||
@upload_required
|
|
||||||
def upload():
|
|
||||||
if not config.config_uploading:
|
|
||||||
abort(404)
|
|
||||||
if request.method == 'POST' and 'btn-upload' in request.files:
|
|
||||||
for requested_file in request.files.getlist("btn-upload"):
|
|
||||||
try:
|
|
||||||
modif_date = False
|
|
||||||
# create the function for sorting...
|
|
||||||
calibre_db.update_title_sort(config)
|
|
||||||
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
|
|
||||||
|
|
||||||
# check if file extension is correct
|
|
||||||
if '.' in requested_file.filename:
|
|
||||||
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
|
||||||
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
|
|
||||||
flash(
|
|
||||||
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
|
|
||||||
ext=file_ext), category="error")
|
|
||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
|
||||||
else:
|
|
||||||
flash(_('File to be uploaded must have an extension'), category="error")
|
|
||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
|
||||||
|
|
||||||
# extract metadata from file
|
|
||||||
try:
|
|
||||||
meta = uploader.upload(requested_file, config.config_rarfile_location)
|
|
||||||
except (IOError, OSError):
|
|
||||||
log.error("File %s could not saved to temp dir", requested_file.filename)
|
|
||||||
flash(_(u"File %(filename)s could not saved to temp dir",
|
|
||||||
filename= requested_file.filename), category="error")
|
|
||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
|
||||||
title = meta.title
|
|
||||||
authr = meta.author
|
|
||||||
|
|
||||||
|
def prepare_authors_on_upload(title, authr):
|
||||||
if title != _(u'Unknown') and authr != _(u'Unknown'):
|
if title != _(u'Unknown') and authr != _(u'Unknown'):
|
||||||
entry = calibre_db.check_exists_book(authr, title)
|
entry = calibre_db.check_exists_book(authr, title)
|
||||||
if entry:
|
if entry:
|
||||||
@@ -851,12 +919,20 @@ def upload():
|
|||||||
sort_author = stored_author.sort
|
sort_author = stored_author.sort
|
||||||
sort_authors_list.append(sort_author)
|
sort_authors_list.append(sort_author)
|
||||||
sort_authors = ' & '.join(sort_authors_list)
|
sort_authors = ' & '.join(sort_authors_list)
|
||||||
|
return sort_authors, input_authors, db_author
|
||||||
|
|
||||||
|
|
||||||
|
def create_book_on_upload(modif_date, meta):
|
||||||
|
title = meta.title
|
||||||
|
authr = meta.author
|
||||||
|
sort_authors, input_authors, db_author = prepare_authors_on_upload(title, authr)
|
||||||
|
|
||||||
title_dir = helper.get_valid_filename(title)
|
title_dir = helper.get_valid_filename(title)
|
||||||
author_dir = helper.get_valid_filename(db_author.name)
|
author_dir = helper.get_valid_filename(db_author.name)
|
||||||
|
|
||||||
# combine path and normalize path from windows systems
|
# combine path and normalize path from windows systems
|
||||||
path = os.path.join(author_dir, title_dir).replace('\\', '/')
|
path = os.path.join(author_dir, title_dir).replace('\\', '/')
|
||||||
|
|
||||||
# Calibre adds books with utc as timezone
|
# Calibre adds books with utc as timezone
|
||||||
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
|
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
|
||||||
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
|
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
|
||||||
@@ -873,6 +949,9 @@ def upload():
|
|||||||
# handle tags
|
# handle tags
|
||||||
modif_date |= edit_book_tags(meta.tags, db_book)
|
modif_date |= edit_book_tags(meta.tags, db_book)
|
||||||
|
|
||||||
|
# handle publisher
|
||||||
|
modif_date |= edit_book_publisher(meta.publisher, db_book)
|
||||||
|
|
||||||
# handle series
|
# handle series
|
||||||
modif_date |= edit_book_series(meta.series, db_book)
|
modif_date |= edit_book_series(meta.series, db_book)
|
||||||
|
|
||||||
@@ -884,19 +963,33 @@ def upload():
|
|||||||
|
|
||||||
# flush content, get db_book.id available
|
# flush content, get db_book.id available
|
||||||
calibre_db.session.flush()
|
calibre_db.session.flush()
|
||||||
|
return db_book, input_authors, title_dir
|
||||||
|
|
||||||
# Comments needs book id therfore only possible after flush
|
def file_handling_on_upload(requested_file):
|
||||||
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
|
# check if file extension is correct
|
||||||
|
if '.' in requested_file.filename:
|
||||||
|
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
||||||
|
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
|
||||||
|
flash(
|
||||||
|
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
|
||||||
|
ext=file_ext), category="error")
|
||||||
|
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
|
else:
|
||||||
|
flash(_('File to be uploaded must have an extension'), category="error")
|
||||||
|
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
|
|
||||||
book_id = db_book.id
|
# extract metadata from file
|
||||||
title = db_book.title
|
try:
|
||||||
|
meta = uploader.upload(requested_file, config.config_rarfile_location)
|
||||||
|
except (IOError, OSError):
|
||||||
|
log.error("File %s could not saved to temp dir", requested_file.filename)
|
||||||
|
flash(_(u"File %(filename)s could not saved to temp dir",
|
||||||
|
filename=requested_file.filename), category="error")
|
||||||
|
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
|
return meta, None
|
||||||
|
|
||||||
error = helper.update_dir_structure_file(book_id,
|
|
||||||
config.config_calibre_dir,
|
|
||||||
input_authors[0],
|
|
||||||
meta.file_path,
|
|
||||||
title_dir + meta.extension)
|
|
||||||
|
|
||||||
|
def move_coverfile(meta, db_book):
|
||||||
# move cover to final directory, including book id
|
# move cover to final directory, including book id
|
||||||
if meta.cover:
|
if meta.cover:
|
||||||
coverfile = meta.cover
|
coverfile = meta.cover
|
||||||
@@ -913,6 +1006,41 @@ def upload():
|
|||||||
error=e),
|
error=e),
|
||||||
category="error")
|
category="error")
|
||||||
|
|
||||||
|
|
||||||
|
@editbook.route("/upload", methods=["GET", "POST"])
|
||||||
|
@login_required_if_no_ano
|
||||||
|
@upload_required
|
||||||
|
def upload():
|
||||||
|
if not config.config_uploading:
|
||||||
|
abort(404)
|
||||||
|
if request.method == 'POST' and 'btn-upload' in request.files:
|
||||||
|
for requested_file in request.files.getlist("btn-upload"):
|
||||||
|
try:
|
||||||
|
modif_date = False
|
||||||
|
# create the function for sorting...
|
||||||
|
calibre_db.update_title_sort(config)
|
||||||
|
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
|
||||||
|
|
||||||
|
meta, error = file_handling_on_upload(requested_file)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta)
|
||||||
|
|
||||||
|
# Comments needs book id therefore only possible after flush
|
||||||
|
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
|
||||||
|
|
||||||
|
book_id = db_book.id
|
||||||
|
title = db_book.title
|
||||||
|
|
||||||
|
error = helper.update_dir_structure_file(book_id,
|
||||||
|
config.config_calibre_dir,
|
||||||
|
input_authors[0],
|
||||||
|
meta.file_path,
|
||||||
|
title_dir + meta.extension.lower())
|
||||||
|
|
||||||
|
move_coverfile(meta, db_book)
|
||||||
|
|
||||||
# save data to database, reread data
|
# save data to database, reread data
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
|
|
||||||
@@ -920,9 +1048,9 @@ def upload():
|
|||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
if error:
|
if error:
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
uploadText=_(u"File %(file)s uploaded", file=title)
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
|
||||||
WorkerThread.add(current_user.nickname, TaskUpload(
|
uploadText = _(u"File %(file)s uploaded", file=link)
|
||||||
"<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>"))
|
WorkerThread.add(current_user.name, TaskUpload(uploadText))
|
||||||
|
|
||||||
if len(request.files.getlist("btn-upload")) < 2:
|
if len(request.files.getlist("btn-upload")) < 2:
|
||||||
if current_user.role_edit() or current_user.role_admin():
|
if current_user.role_edit() or current_user.role_admin():
|
||||||
@@ -951,7 +1079,7 @@ def convert_bookformat(book_id):
|
|||||||
|
|
||||||
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
|
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
|
||||||
rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(),
|
rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(),
|
||||||
book_format_to.upper(), current_user.nickname)
|
book_format_to.upper(), current_user.name)
|
||||||
|
|
||||||
if rtn is None:
|
if rtn is None:
|
||||||
flash(_(u"Book successfully queued for converting to %(book_format)s",
|
flash(_(u"Book successfully queued for converting to %(book_format)s",
|
||||||
@@ -961,61 +1089,113 @@ def convert_bookformat(book_id):
|
|||||||
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
||||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
|
|
||||||
|
@editbook.route("/ajax/getcustomenum/<int:c_id>")
|
||||||
|
@login_required
|
||||||
|
def table_get_custom_enum(c_id):
|
||||||
|
ret = list()
|
||||||
|
cc = (calibre_db.session.query(db.Custom_Columns)
|
||||||
|
.filter(db.Custom_Columns.id == c_id)
|
||||||
|
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).one_or_none())
|
||||||
|
ret.append({'value': "", 'text': ""})
|
||||||
|
for idx, en in enumerate(cc.get_display_dict()['enum_values']):
|
||||||
|
ret.append({'value': en, 'text': en})
|
||||||
|
return json.dumps(ret)
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
def edit_list_book(param):
|
def edit_list_book(param):
|
||||||
vals = request.form.to_dict()
|
vals = request.form.to_dict()
|
||||||
book = calibre_db.get_book(vals['pk'])
|
book = calibre_db.get_book(vals['pk'])
|
||||||
|
ret = ""
|
||||||
if param =='series_index':
|
if param =='series_index':
|
||||||
edit_book_series_index(vals['value'], book)
|
edit_book_series_index(vals['value'], book)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json')
|
||||||
elif param =='tags':
|
elif param =='tags':
|
||||||
edit_book_tags(vals['value'], book)
|
edit_book_tags(vals['value'], book)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='series':
|
elif param =='series':
|
||||||
edit_book_series(vals['value'], book)
|
edit_book_series(vals['value'], book)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='publishers':
|
elif param =='publishers':
|
||||||
vals['publisher'] = vals['value']
|
edit_book_publisher(vals['value'], book)
|
||||||
edit_book_publisher(vals, book)
|
ret = Response(json.dumps({'success': True,
|
||||||
|
'newValue': ', '.join([publisher.name for publisher in book.publishers])}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='languages':
|
elif param =='languages':
|
||||||
edit_book_languages(vals['value'], book)
|
invalid = list()
|
||||||
|
edit_book_languages(vals['value'], book, invalid=invalid)
|
||||||
|
if invalid:
|
||||||
|
ret = Response(json.dumps({'success': False,
|
||||||
|
'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}),
|
||||||
|
mimetype='application/json')
|
||||||
|
else:
|
||||||
|
lang_names = list()
|
||||||
|
for lang in book.languages:
|
||||||
|
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='author_sort':
|
elif param =='author_sort':
|
||||||
book.author_sort = vals['value']
|
book.author_sort = vals['value']
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param == 'title':
|
elif param == 'title':
|
||||||
book.title = vals['value']
|
sort = book.sort
|
||||||
|
handle_title_on_edit(book, vals.get('value', ""))
|
||||||
helper.update_dir_stucture(book.id, config.config_calibre_dir)
|
helper.update_dir_stucture(book.id, config.config_calibre_dir)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': book.title}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='sort':
|
elif param =='sort':
|
||||||
book.sort = vals['value']
|
book.sort = vals['value']
|
||||||
# ToDo: edit books
|
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
|
||||||
|
mimetype='application/json')
|
||||||
|
elif param =='comments':
|
||||||
|
edit_book_comments(vals['value'], book)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': book.comments[0].text}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='authors':
|
elif param =='authors':
|
||||||
input_authors = vals['value'].split('&')
|
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
|
||||||
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
|
|
||||||
modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
|
||||||
sort_authors_list = list()
|
|
||||||
for inp in input_authors:
|
|
||||||
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
|
||||||
if not stored_author:
|
|
||||||
stored_author = helper.get_sorted_author(inp)
|
|
||||||
else:
|
|
||||||
stored_author = stored_author.sort
|
|
||||||
sort_authors_list.append(helper.get_sorted_author(stored_author))
|
|
||||||
sort_authors = ' & '.join(sort_authors_list)
|
|
||||||
if book.author_sort != sort_authors:
|
|
||||||
book.author_sort = sort_authors
|
|
||||||
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
|
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
|
||||||
|
ret = Response(json.dumps({'success': True,
|
||||||
|
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
|
||||||
|
mimetype='application/json')
|
||||||
|
elif param.startswith("custom_column_"):
|
||||||
|
new_val = dict()
|
||||||
|
new_val[param] = vals['value']
|
||||||
|
edit_single_cc_data(book.id, book, param[14:], new_val)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': vals['value']}),
|
||||||
|
mimetype='application/json')
|
||||||
|
|
||||||
book.last_modified = datetime.utcnow()
|
book.last_modified = datetime.utcnow()
|
||||||
|
try:
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
return ""
|
# revert change for sort if automatic fields link is deactivated
|
||||||
|
if param == 'title' and vals.get('checkT') == "false":
|
||||||
|
book.sort = sort
|
||||||
|
calibre_db.session.commit()
|
||||||
|
except (OperationalError, IntegrityError) as e:
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
log.error("Database error: %s", e)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
||||||
@login_required
|
@login_required
|
||||||
def get_sorted_entry(field, bookid):
|
def get_sorted_entry(field, bookid):
|
||||||
if field == 'title' or field == 'authors':
|
if field in ['title', 'authors', 'sort', 'author_sort']:
|
||||||
book = calibre_db.get_filtered_book(bookid)
|
book = calibre_db.get_filtered_book(bookid)
|
||||||
if book:
|
if book:
|
||||||
if field == 'title':
|
if field == 'title':
|
||||||
return json.dumps({'sort': book.sort})
|
return json.dumps({'sort': book.sort})
|
||||||
elif field == 'authors':
|
elif field == 'authors':
|
||||||
return json.dumps({'author_sort': book.author_sort})
|
return json.dumps({'author_sort': book.author_sort})
|
||||||
|
if field == 'sort':
|
||||||
|
return json.dumps({'sort': book.title})
|
||||||
|
if field == 'author_sort':
|
||||||
|
return json.dumps({'author_sort': book.author})
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -1067,6 +1247,46 @@ def merge_list_book():
|
|||||||
element.format,
|
element.format,
|
||||||
element.uncompressed_size,
|
element.uncompressed_size,
|
||||||
to_name))
|
to_name))
|
||||||
delete_book(from_book.id,"", True) # json_resp =
|
delete_book(from_book.id,"", True)
|
||||||
|
return json.dumps({'success': True})
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@editbook.route("/ajax/xchange", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@edit_required
|
||||||
|
def table_xchange_author_title():
|
||||||
|
vals = request.get_json().get('xchange')
|
||||||
|
if vals:
|
||||||
|
for val in vals:
|
||||||
|
modif_date = False
|
||||||
|
book = calibre_db.get_book(val)
|
||||||
|
authors = book.title
|
||||||
|
entries = calibre_db.order_authors(book)
|
||||||
|
author_names = []
|
||||||
|
for authr in entries.authors:
|
||||||
|
author_names.append(authr.name.replace('|', ','))
|
||||||
|
|
||||||
|
title_change = handle_title_on_edit(book, " ".join(author_names))
|
||||||
|
input_authors, authorchange = handle_author_on_edit(book, authors)
|
||||||
|
if authorchange or title_change:
|
||||||
|
edited_books_id = book.id
|
||||||
|
modif_date = True
|
||||||
|
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
|
|
||||||
|
if edited_books_id:
|
||||||
|
helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0])
|
||||||
|
if modif_date:
|
||||||
|
book.last_modified = datetime.utcnow()
|
||||||
|
try:
|
||||||
|
calibre_db.session.commit()
|
||||||
|
except (OperationalError, IntegrityError) as e:
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
log.error("Database error: %s", e)
|
||||||
|
return json.dumps({'success': False})
|
||||||
|
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
return json.dumps({'success': True})
|
return json.dumps({'success': True})
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
+33
-25
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -87,18 +86,29 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
lang = epub_metadata['language'].split('-', 1)[0].lower()
|
lang = epub_metadata['language'].split('-', 1)[0].lower()
|
||||||
epub_metadata['language'] = isoLanguages.get_lang3(lang)
|
epub_metadata['language'] = isoLanguages.get_lang3(lang)
|
||||||
|
|
||||||
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
|
epub_metadata = parse_epbub_series(ns, tree, epub_metadata)
|
||||||
if len(series) > 0:
|
|
||||||
epub_metadata['series'] = series[0]
|
|
||||||
else:
|
|
||||||
epub_metadata['series'] = ''
|
|
||||||
|
|
||||||
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
|
coverfile = parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path)
|
||||||
if len(series_id) > 0:
|
|
||||||
epub_metadata['series_id'] = series_id[0]
|
|
||||||
else:
|
|
||||||
epub_metadata['series_id'] = '1'
|
|
||||||
|
|
||||||
|
if not epub_metadata['title']:
|
||||||
|
title = original_file_name
|
||||||
|
else:
|
||||||
|
title = epub_metadata['title']
|
||||||
|
|
||||||
|
return BookMeta(
|
||||||
|
file_path=tmp_file_path,
|
||||||
|
extension=original_file_extension,
|
||||||
|
title=title.encode('utf-8').decode('utf-8'),
|
||||||
|
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
|
||||||
|
cover=coverfile,
|
||||||
|
description=epub_metadata['description'],
|
||||||
|
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
|
||||||
|
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
|
||||||
|
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
|
||||||
|
languages=epub_metadata['language'],
|
||||||
|
publisher="")
|
||||||
|
|
||||||
|
def parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path):
|
||||||
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
||||||
coverfile = None
|
coverfile = None
|
||||||
if len(coversection) > 0:
|
if len(coversection) > 0:
|
||||||
@@ -126,20 +136,18 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
coverfile = extractCover(epubZip, filename, "", tmp_file_path)
|
coverfile = extractCover(epubZip, filename, "", tmp_file_path)
|
||||||
else:
|
else:
|
||||||
coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path)
|
coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path)
|
||||||
|
return coverfile
|
||||||
|
|
||||||
if not epub_metadata['title']:
|
def parse_epbub_series(ns, tree, epub_metadata):
|
||||||
title = original_file_name
|
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
|
||||||
|
if len(series) > 0:
|
||||||
|
epub_metadata['series'] = series[0]
|
||||||
else:
|
else:
|
||||||
title = epub_metadata['title']
|
epub_metadata['series'] = ''
|
||||||
|
|
||||||
return BookMeta(
|
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
|
||||||
file_path=tmp_file_path,
|
if len(series_id) > 0:
|
||||||
extension=original_file_extension,
|
epub_metadata['series_id'] = series_id[0]
|
||||||
title=title.encode('utf-8').decode('utf-8'),
|
else:
|
||||||
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
|
epub_metadata['series_id'] = '1'
|
||||||
cover=coverfile,
|
return epub_metadata
|
||||||
description=epub_metadata['description'],
|
|
||||||
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
|
|
||||||
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
|
|
||||||
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
|
|
||||||
languages=epub_metadata['language'])
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ def error_http(error):
|
|||||||
error_code="Error {0}".format(error.code),
|
error_code="Error {0}".format(error.code),
|
||||||
error_name=error.name,
|
error_name=error.name,
|
||||||
issue=False,
|
issue=False,
|
||||||
|
unconfigured=not config.db_configured,
|
||||||
instance=config.config_calibre_web_title
|
instance=config.config_calibre_web_title
|
||||||
), error.code
|
), error.code
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ def internal_error(error):
|
|||||||
error_code="Internal Server Error",
|
error_code="Internal Server Error",
|
||||||
error_name=str(error),
|
error_name=str(error),
|
||||||
issue=True,
|
issue=True,
|
||||||
|
unconfigured=False,
|
||||||
error_stack=traceback.format_exc().split("\n"),
|
error_stack=traceback.format_exc().split("\n"),
|
||||||
instance=config.config_calibre_web_title
|
instance=config.config_calibre_web_title
|
||||||
), 500
|
), 500
|
||||||
@@ -60,14 +62,8 @@ def init_errorhandler():
|
|||||||
if services.ldap:
|
if services.ldap:
|
||||||
# Only way of catching the LDAPException upon logging in with LDAP server down
|
# Only way of catching the LDAPException upon logging in with LDAP server down
|
||||||
@app.errorhandler(services.ldap.LDAPException)
|
@app.errorhandler(services.ldap.LDAPException)
|
||||||
|
# pylint: disable=unused-variable
|
||||||
def handle_exception(e):
|
def handle_exception(e):
|
||||||
log.debug('LDAP server not accessible while trying to login to opds feed')
|
log.debug('LDAP server not accessible while trying to login to opds feed')
|
||||||
return error_http(FailedDependency())
|
return error_http(FailedDependency())
|
||||||
|
|
||||||
|
|
||||||
# @app.errorhandler(InvalidRequestError)
|
|
||||||
#@app.errorhandler(OperationalError)
|
|
||||||
#def handle_db_exception(e):
|
|
||||||
# db.session.rollback()
|
|
||||||
# log.error('Database request error: %s',e)
|
|
||||||
# return internal_error(InternalServerError(e))
|
|
||||||
|
|||||||
+15
-15
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
@@ -29,52 +28,53 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
|||||||
'l': 'http://www.w3.org/1999/xlink',
|
'l': 'http://www.w3.org/1999/xlink',
|
||||||
}
|
}
|
||||||
|
|
||||||
fb2_file = open(tmp_file_path)
|
fb2_file = open(tmp_file_path, encoding="utf-8")
|
||||||
tree = etree.fromstring(fb2_file.read())
|
tree = etree.fromstring(fb2_file.read().encode())
|
||||||
|
|
||||||
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
||||||
|
|
||||||
def get_author(element):
|
def get_author(element):
|
||||||
last_name = element.xpath('fb:last-name/text()', namespaces=ns)
|
last_name = element.xpath('fb:last-name/text()', namespaces=ns)
|
||||||
if len(last_name):
|
if len(last_name):
|
||||||
last_name = last_name[0].encode('utf-8')
|
last_name = last_name[0]
|
||||||
else:
|
else:
|
||||||
last_name = u''
|
last_name = u''
|
||||||
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
|
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
|
||||||
if len(middle_name):
|
if len(middle_name):
|
||||||
middle_name = middle_name[0].encode('utf-8')
|
middle_name = middle_name[0]
|
||||||
else:
|
else:
|
||||||
middle_name = u''
|
middle_name = u''
|
||||||
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
|
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
|
||||||
if len(first_name):
|
if len(first_name):
|
||||||
first_name = first_name[0].encode('utf-8')
|
first_name = first_name[0]
|
||||||
else:
|
else:
|
||||||
first_name = u''
|
first_name = u''
|
||||||
return (first_name.decode('utf-8') + u' '
|
return (first_name + u' '
|
||||||
+ middle_name.decode('utf-8') + u' '
|
+ middle_name + u' '
|
||||||
+ last_name.decode('utf-8')).encode('utf-8')
|
+ last_name)
|
||||||
|
|
||||||
author = str(", ".join(map(get_author, authors)))
|
author = str(", ".join(map(get_author, authors)))
|
||||||
|
|
||||||
title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
|
title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
|
||||||
if len(title):
|
if len(title):
|
||||||
title = str(title[0].encode('utf-8'))
|
title = str(title[0])
|
||||||
else:
|
else:
|
||||||
title = u''
|
title = u''
|
||||||
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
|
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
|
||||||
if len(description):
|
if len(description):
|
||||||
description = str(description[0].encode('utf-8'))
|
description = str(description[0])
|
||||||
else:
|
else:
|
||||||
description = u''
|
description = u''
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
extension=original_file_extension,
|
extension=original_file_extension,
|
||||||
title=title.decode('utf-8'),
|
title=title,
|
||||||
author=author.decode('utf-8'),
|
author=author,
|
||||||
cover=None,
|
cover=None,
|
||||||
description=description.decode('utf-8'),
|
description=description,
|
||||||
tags="",
|
tags="",
|
||||||
series="",
|
series="",
|
||||||
series_id="",
|
series_id="",
|
||||||
languages="")
|
languages="",
|
||||||
|
publisher="")
|
||||||
|
|||||||
+12
-14
@@ -20,9 +20,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -34,7 +32,7 @@ from flask import Blueprint, flash, request, redirect, url_for, abort
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
from . import logger, gdriveutils, config, ub, calibre_db
|
from . import logger, gdriveutils, config, ub, calibre_db, csrf
|
||||||
from .admin import admin_required
|
from .admin import admin_required
|
||||||
|
|
||||||
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||||
@@ -47,7 +45,7 @@ except ImportError as err:
|
|||||||
|
|
||||||
current_milli_time = lambda: int(round(time() * 1000))
|
current_milli_time = lambda: int(round(time() * 1000))
|
||||||
|
|
||||||
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
|
gdrive_watch_callback_token = 'target=calibreweb-watch_files' #nosec
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/authenticate")
|
@gdrive.route("/authenticate")
|
||||||
@@ -74,7 +72,7 @@ def google_drive_callback():
|
|||||||
f.write(credentials.to_json())
|
f.write(credentials.to_json())
|
||||||
except (ValueError, AttributeError) as error:
|
except (ValueError, AttributeError) as error:
|
||||||
log.error(error)
|
log.error(error)
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.db_configuration'))
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/watch/subscribe")
|
@gdrive.route("/watch/subscribe")
|
||||||
@@ -99,7 +97,7 @@ def watch_gdrive():
|
|||||||
else:
|
else:
|
||||||
flash(reason['message'], category="error")
|
flash(reason['message'], category="error")
|
||||||
|
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.db_configuration'))
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/watch/revoke")
|
@gdrive.route("/watch/revoke")
|
||||||
@@ -115,9 +113,10 @@ def revoke_watch_gdrive():
|
|||||||
pass
|
pass
|
||||||
config.config_google_drive_watch_changes_response = {}
|
config.config_google_drive_watch_changes_response = {}
|
||||||
config.save()
|
config.save()
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.db_configuration'))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
@csrf.exempt
|
||||||
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
||||||
def on_received_watch_confirmation():
|
def on_received_watch_confirmation():
|
||||||
if not config.config_google_drive_watch_changes_response:
|
if not config.config_google_drive_watch_changes_response:
|
||||||
@@ -137,12 +136,9 @@ def on_received_watch_confirmation():
|
|||||||
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
|
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
|
||||||
log.debug('%r', response)
|
log.debug('%r', response)
|
||||||
if response:
|
if response:
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
|
|
||||||
else:
|
|
||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
|
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
||||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||||
if not os.path.isdir(tmp_dir):
|
if not os.path.isdir(tmp_dir):
|
||||||
os.mkdir(tmp_dir)
|
os.mkdir(tmp_dir)
|
||||||
@@ -155,6 +151,8 @@ def on_received_watch_confirmation():
|
|||||||
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
||||||
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
||||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
return ''
|
return ''
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|||||||
+40
-10
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
@@ -28,8 +27,13 @@ from sqlalchemy import create_engine
|
|||||||
from sqlalchemy import Column, UniqueConstraint
|
from sqlalchemy import Column, UniqueConstraint
|
||||||
from sqlalchemy import String, Integer
|
from sqlalchemy import String, Integer
|
||||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
|
try:
|
||||||
|
# Compatibility with sqlalchemy 2.0
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
except ImportError:
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||||
|
from sqlalchemy.sql.expression import text
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from apiclient import errors
|
from apiclient import errors
|
||||||
@@ -164,7 +168,7 @@ class PermissionAdded(Base):
|
|||||||
def migrate():
|
def migrate():
|
||||||
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
|
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
|
||||||
PermissionAdded.__table__.create(bind = engine)
|
PermissionAdded.__table__.create(bind = engine)
|
||||||
for sql in session.execute("select sql from sqlite_master where type='table'"):
|
for sql in session.execute(text("select sql from sqlite_master where type='table'")):
|
||||||
if 'CREATE TABLE gdrive_ids' in sql[0]:
|
if 'CREATE TABLE gdrive_ids' in sql[0]:
|
||||||
currUniqueConstraint = 'UNIQUE (gdrive_id)'
|
currUniqueConstraint = 'UNIQUE (gdrive_id)'
|
||||||
if currUniqueConstraint in sql[0]:
|
if currUniqueConstraint in sql[0]:
|
||||||
@@ -198,8 +202,8 @@ def getDrive(drive=None, gauth=None):
|
|||||||
gauth.Refresh()
|
gauth.Refresh()
|
||||||
except RefreshError as e:
|
except RefreshError as e:
|
||||||
log.error("Google Drive error: %s", e)
|
log.error("Google Drive error: %s", e)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
else:
|
else:
|
||||||
# Initialize the saved creds
|
# Initialize the saved creds
|
||||||
gauth.Authorize()
|
gauth.Authorize()
|
||||||
@@ -217,7 +221,7 @@ def listRootFolders():
|
|||||||
drive = getDrive(Gdrive.Instance().drive)
|
drive = getDrive(Gdrive.Instance().drive)
|
||||||
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
|
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
|
||||||
fileList = drive.ListFile({'q': folder}).GetList()
|
fileList = drive.ListFile({'q': folder}).GetList()
|
||||||
except (ServerNotFoundError, ssl.SSLError) as e:
|
except (ServerNotFoundError, ssl.SSLError, RefreshError) as e:
|
||||||
log.info("GDrive Error %s" % e)
|
log.info("GDrive Error %s" % e)
|
||||||
fileList = []
|
fileList = []
|
||||||
return fileList
|
return fileList
|
||||||
@@ -253,7 +257,12 @@ def getEbooksFolderId(drive=None):
|
|||||||
log.error('Error gDrive, root ID not found')
|
log.error('Error gDrive, root ID not found')
|
||||||
gDriveId.path = '/'
|
gDriveId.path = '/'
|
||||||
session.merge(gDriveId)
|
session.merge(gDriveId)
|
||||||
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
return gDriveId.gdrive_id
|
return gDriveId.gdrive_id
|
||||||
|
|
||||||
|
|
||||||
@@ -268,6 +277,7 @@ def getFile(pathId, fileName, drive):
|
|||||||
|
|
||||||
def getFolderId(path, drive):
|
def getFolderId(path, drive):
|
||||||
# drive = getDrive(drive)
|
# drive = getDrive(drive)
|
||||||
|
try:
|
||||||
currentFolderId = getEbooksFolderId(drive)
|
currentFolderId = getEbooksFolderId(drive)
|
||||||
sqlCheckPath = path if path[-1] == '/' else path + '/'
|
sqlCheckPath = path if path[-1] == '/' else path + '/'
|
||||||
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
|
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
|
||||||
@@ -299,6 +309,10 @@ def getFolderId(path, drive):
|
|||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
currentFolderId = storedPathName.gdrive_id
|
currentFolderId = storedPathName.gdrive_id
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
return currentFolderId
|
return currentFolderId
|
||||||
|
|
||||||
|
|
||||||
@@ -342,7 +356,7 @@ def moveGdriveFolderRemote(origin_file, target_folder):
|
|||||||
addParents=gFileTargetDir['id'],
|
addParents=gFileTargetDir['id'],
|
||||||
removeParents=previous_parents,
|
removeParents=previous_parents,
|
||||||
fields='id, parents').execute()
|
fields='id, parents').execute()
|
||||||
# if previous_parents has no childs anymore, delete original fileparent
|
# if previous_parents has no children anymore, delete original fileparent
|
||||||
if len(children['items']) == 1:
|
if len(children['items']) == 1:
|
||||||
deleteDatabaseEntry(previous_parents)
|
deleteDatabaseEntry(previous_parents)
|
||||||
drive.auth.service.files().delete(fileId=previous_parents).execute()
|
drive.auth.service.files().delete(fileId=previous_parents).execute()
|
||||||
@@ -493,8 +507,8 @@ def getChangeById (drive, change_id):
|
|||||||
except (errors.HttpError) as error:
|
except (errors.HttpError) as error:
|
||||||
log.error(error)
|
log.error(error)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.error(e)
|
log.error(ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -503,9 +517,10 @@ def deleteDatabaseOnChange():
|
|||||||
try:
|
try:
|
||||||
session.query(GdriveId).delete()
|
session.query(GdriveId).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError) as ex:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
log.info(u"GDrive DB is not Writeable")
|
log.debug('Database error: %s', ex)
|
||||||
|
log.error(u"GDrive DB is not Writeable")
|
||||||
|
|
||||||
|
|
||||||
def updateGdriveCalibreFromLocal():
|
def updateGdriveCalibreFromLocal():
|
||||||
@@ -520,13 +535,23 @@ def updateDatabaseOnEdit(ID,newPath):
|
|||||||
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
|
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
|
||||||
if storedPathName:
|
if storedPathName:
|
||||||
storedPathName.path = sqlCheckPath
|
storedPathName.path = sqlCheckPath
|
||||||
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
# Deletes the hashes in database of deleted book
|
# Deletes the hashes in database of deleted book
|
||||||
def deleteDatabaseEntry(ID):
|
def deleteDatabaseEntry(ID):
|
||||||
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
|
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
|
||||||
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
# Gets cover file from gdrive
|
# Gets cover file from gdrive
|
||||||
@@ -543,7 +568,12 @@ def get_cover_via_gdrive(cover_path):
|
|||||||
permissionAdded = PermissionAdded()
|
permissionAdded = PermissionAdded()
|
||||||
permissionAdded.gdrive_id = df['id']
|
permissionAdded.gdrive_id = df['id']
|
||||||
session.add(permissionAdded)
|
session.add(permissionAdded)
|
||||||
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
return df.metadata.get('webContentLink')
|
return df.metadata.get('webContentLink')
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
+98
-82
@@ -17,7 +17,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
@@ -35,9 +34,10 @@ from babel.units import format_unit
|
|||||||
from flask import send_from_directory, make_response, redirect, abort, url_for
|
from flask import send_from_directory, make_response, redirect, abort, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from sqlalchemy.sql.expression import true, false, and_, text
|
from sqlalchemy.sql.expression import true, false, and_, text, func
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
from markupsafe import escape
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@@ -68,6 +68,7 @@ try:
|
|||||||
except (ImportError, RuntimeError) as e:
|
except (ImportError, RuntimeError) as e:
|
||||||
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
|
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
|
||||||
use_IM = False
|
use_IM = False
|
||||||
|
MissingDelegateError = BaseException
|
||||||
|
|
||||||
|
|
||||||
# Convert existing book entry to new format
|
# Convert existing book entry to new format
|
||||||
@@ -96,10 +97,11 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
|||||||
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
||||||
else:
|
else:
|
||||||
settings = dict()
|
settings = dict()
|
||||||
txt = (u"%s -> %s: %s" % (
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss
|
||||||
old_book_format,
|
txt = u"{} -> {}: {}".format(
|
||||||
new_book_format,
|
old_book_format.upper(),
|
||||||
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + book.title + "</a>"))
|
new_book_format.upper(),
|
||||||
|
link)
|
||||||
settings['old_book_format'] = old_book_format
|
settings['old_book_format'] = old_book_format
|
||||||
settings['new_book_format'] = new_book_format
|
settings['new_book_format'] = new_book_format
|
||||||
WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, kindle_mail, user_id))
|
WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, kindle_mail, user_id))
|
||||||
@@ -133,50 +135,11 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
|||||||
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
|
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
|
||||||
text=txt
|
text=txt
|
||||||
))
|
))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def check_send_to_kindle(entry):
|
def check_send_to_kindle_with_converter(formats):
|
||||||
"""
|
|
||||||
returns all available book formats for sending to Kindle
|
|
||||||
"""
|
|
||||||
if len(entry.data):
|
|
||||||
bookformats = list()
|
bookformats = list()
|
||||||
if not config.config_converterpath:
|
|
||||||
# no converter - only for mobi and pdf formats
|
|
||||||
for ele in iter(entry.data):
|
|
||||||
if ele.uncompressed_size < config.mail_size:
|
|
||||||
if 'MOBI' in ele.format:
|
|
||||||
bookformats.append({'format': 'Mobi',
|
|
||||||
'convert': 0,
|
|
||||||
'text': _('Send %(format)s to Kindle', format='Mobi')})
|
|
||||||
if 'PDF' in ele.format:
|
|
||||||
bookformats.append({'format': 'Pdf',
|
|
||||||
'convert': 0,
|
|
||||||
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
|
||||||
if 'AZW' in ele.format:
|
|
||||||
bookformats.append({'format': 'Azw',
|
|
||||||
'convert': 0,
|
|
||||||
'text': _('Send %(format)s to Kindle', format='Azw')})
|
|
||||||
else:
|
|
||||||
formats = list()
|
|
||||||
for ele in iter(entry.data):
|
|
||||||
if ele.uncompressed_size < config.mail_size:
|
|
||||||
formats.append(ele.format)
|
|
||||||
if 'MOBI' in formats:
|
|
||||||
bookformats.append({'format': 'Mobi',
|
|
||||||
'convert': 0,
|
|
||||||
'text': _('Send %(format)s to Kindle', format='Mobi')})
|
|
||||||
if 'AZW' in formats:
|
|
||||||
bookformats.append({'format': 'Azw',
|
|
||||||
'convert': 0,
|
|
||||||
'text': _('Send %(format)s to Kindle', format='Azw')})
|
|
||||||
if 'PDF' in formats:
|
|
||||||
bookformats.append({'format': 'Pdf',
|
|
||||||
'convert': 0,
|
|
||||||
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
|
||||||
if config.config_converterpath:
|
|
||||||
if 'EPUB' in formats and 'MOBI' not in formats:
|
if 'EPUB' in formats and 'MOBI' not in formats:
|
||||||
bookformats.append({'format': 'Mobi',
|
bookformats.append({'format': 'Mobi',
|
||||||
'convert': 1,
|
'convert': 1,
|
||||||
@@ -190,6 +153,33 @@ def check_send_to_kindle(entry):
|
|||||||
orig='Azw3',
|
orig='Azw3',
|
||||||
format='Mobi')})
|
format='Mobi')})
|
||||||
return bookformats
|
return bookformats
|
||||||
|
|
||||||
|
|
||||||
|
def check_send_to_kindle(entry):
|
||||||
|
"""
|
||||||
|
returns all available book formats for sending to Kindle
|
||||||
|
"""
|
||||||
|
formats = list()
|
||||||
|
bookformats = list()
|
||||||
|
if len(entry.data):
|
||||||
|
for ele in iter(entry.data):
|
||||||
|
if ele.uncompressed_size < config.mail_size:
|
||||||
|
formats.append(ele.format)
|
||||||
|
if 'MOBI' in formats:
|
||||||
|
bookformats.append({'format': 'Mobi',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Mobi')})
|
||||||
|
if 'PDF' in formats:
|
||||||
|
bookformats.append({'format': 'Pdf',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
||||||
|
if 'AZW' in formats:
|
||||||
|
bookformats.append({'format': 'Azw',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Azw')})
|
||||||
|
if config.config_converterpath:
|
||||||
|
bookformats.extend(check_send_to_kindle_with_converter(formats))
|
||||||
|
return bookformats
|
||||||
else:
|
else:
|
||||||
log.error(u'Cannot find book entry %d', entry.id)
|
log.error(u'Cannot find book entry %d', entry.id)
|
||||||
return None
|
return None
|
||||||
@@ -198,7 +188,7 @@ def check_send_to_kindle(entry):
|
|||||||
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
|
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
|
||||||
# list with supported formats
|
# list with supported formats
|
||||||
def check_read_formats(entry):
|
def check_read_formats(entry):
|
||||||
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR'}
|
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'}
|
||||||
bookformats = list()
|
bookformats = list()
|
||||||
if len(entry.data):
|
if len(entry.data):
|
||||||
for ele in iter(entry.data):
|
for ele in iter(entry.data):
|
||||||
@@ -225,9 +215,11 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
|
|||||||
for entry in iter(book.data):
|
for entry in iter(book.data):
|
||||||
if entry.format.upper() == book_format.upper():
|
if entry.format.upper() == book_format.upper():
|
||||||
converted_file_name = entry.name + '.' + book_format.lower()
|
converted_file_name = entry.name + '.' + book_format.lower()
|
||||||
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
|
||||||
|
EmailText = _(u"%(book)s send to Kindle", book=link)
|
||||||
WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
|
WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
|
||||||
config.get_mail_settings(), kindle_mail,
|
config.get_mail_settings(), kindle_mail,
|
||||||
_(u"E-mail: %(book)s", book=book.title), _(u'This e-mail has been sent via Calibre-Web.')))
|
EmailText, _(u'This e-mail has been sent via Calibre-Web.')))
|
||||||
return
|
return
|
||||||
return _(u"The requested file could not be read. Maybe wrong permissions?")
|
return _(u"The requested file could not be read. Maybe wrong permissions?")
|
||||||
|
|
||||||
@@ -241,16 +233,14 @@ def get_valid_filename(value, replace_whitespace=True):
|
|||||||
value = value[:-1]+u'_'
|
value = value[:-1]+u'_'
|
||||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||||
if use_unidecode:
|
if use_unidecode:
|
||||||
|
if not config.config_unicode_filename:
|
||||||
value = (unidecode.unidecode(value))
|
value = (unidecode.unidecode(value))
|
||||||
else:
|
else:
|
||||||
value = value.replace(u'§', u'SS')
|
value = value.replace(u'§', u'SS')
|
||||||
value = value.replace(u'ß', u'ss')
|
value = value.replace(u'ß', u'ss')
|
||||||
value = unicodedata.normalize('NFKD', value)
|
value = unicodedata.normalize('NFKD', value)
|
||||||
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
|
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
|
||||||
if isinstance(value, str): # Python3 str, Python2 unicode
|
|
||||||
value = re_slugify.sub('', value)
|
value = re_slugify.sub('', value)
|
||||||
else:
|
|
||||||
value = unicode(re_slugify.sub('', value))
|
|
||||||
if replace_whitespace:
|
if replace_whitespace:
|
||||||
# *+:\"/<>? are replaced by _
|
# *+:\"/<>? are replaced by _
|
||||||
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
||||||
@@ -259,10 +249,7 @@ def get_valid_filename(value, replace_whitespace=True):
|
|||||||
value = value[:128].strip()
|
value = value[:128].strip()
|
||||||
if not value:
|
if not value:
|
||||||
raise ValueError("Filename cannot be empty")
|
raise ValueError("Filename cannot be empty")
|
||||||
if sys.version_info.major == 3:
|
|
||||||
return value
|
return value
|
||||||
else:
|
|
||||||
return value.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
def split_authors(values):
|
def split_authors(values):
|
||||||
@@ -340,9 +327,10 @@ def delete_book_file(book, calibrepath, book_format=None):
|
|||||||
except (IOError, OSError) as e:
|
except (IOError, OSError) as e:
|
||||||
log.error("Deleting authorpath for book %s failed: %s", book.id, e)
|
log.error("Deleting authorpath for book %s failed: %s", book.id, e)
|
||||||
return True, None
|
return True, None
|
||||||
else:
|
|
||||||
log.error("Deleting book %s failed, book path not valid: %s", book.id, book.path)
|
log.error("Deleting book %s from database only, book path in database not valid: %s",
|
||||||
return True, _("Deleting book %(id)s, book path not valid: %(path)s",
|
book.id, book.path)
|
||||||
|
return True, _("Deleting book %(id)s from database only, book path in database not valid: %(path)s",
|
||||||
id=book.id,
|
id=book.id,
|
||||||
path=book.path)
|
path=book.path)
|
||||||
|
|
||||||
@@ -393,7 +381,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author, orignal_filepa
|
|||||||
# os.unlink(os.path.normcase(os.path.join(dir_name, file)))
|
# os.unlink(os.path.normcase(os.path.join(dir_name, file)))
|
||||||
# change location in database to new author/title path
|
# change location in database to new author/title path
|
||||||
localbook.path = os.path.join(new_authordir, new_titledir).replace('\\','/')
|
localbook.path = os.path.join(new_authordir, new_titledir).replace('\\','/')
|
||||||
except OSError as ex:
|
except (OSError) as ex:
|
||||||
log.error("Rename title from: %s to %s: %s", path, new_path, ex)
|
log.error("Rename title from: %s to %s: %s", path, new_path, ex)
|
||||||
log.debug(ex, exc_info=True)
|
log.debug(ex, exc_info=True)
|
||||||
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||||
@@ -408,7 +396,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author, orignal_filepa
|
|||||||
file_format.name = new_name
|
file_format.name = new_name
|
||||||
if not orignal_filepath and len(os.listdir(os.path.dirname(path))) == 0:
|
if not orignal_filepath and len(os.listdir(os.path.dirname(path))) == 0:
|
||||||
shutil.rmtree(os.path.dirname(path))
|
shutil.rmtree(os.path.dirname(path))
|
||||||
except OSError as ex:
|
except (OSError) as ex:
|
||||||
log.error("Rename file in path %s to %s: %s", new_path, new_name, ex)
|
log.error("Rename file in path %s to %s: %s", new_path, new_name, ex)
|
||||||
log.debug(ex, exc_info=True)
|
log.debug(ex, exc_info=True)
|
||||||
return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||||
@@ -491,8 +479,8 @@ def reset_password(user_id):
|
|||||||
password = generate_random_password()
|
password = generate_random_password()
|
||||||
existing_user.password = generate_password_hash(password)
|
existing_user.password = generate_password_hash(password)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
send_registration_mail(existing_user.email, existing_user.nickname, password, True)
|
send_registration_mail(existing_user.email, existing_user.name, password, True)
|
||||||
return 1, existing_user.nickname
|
return 1, existing_user.name
|
||||||
except Exception:
|
except Exception:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
return 0, None
|
return 0, None
|
||||||
@@ -501,19 +489,42 @@ def reset_password(user_id):
|
|||||||
def generate_random_password():
|
def generate_random_password():
|
||||||
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
||||||
passlen = 8
|
passlen = 8
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
return "".join(s[ord(c) % len(s)] for c in os.urandom(passlen))
|
|
||||||
else:
|
|
||||||
return "".join(s[c % len(s)] for c in os.urandom(passlen))
|
return "".join(s[c % len(s)] for c in os.urandom(passlen))
|
||||||
|
|
||||||
|
|
||||||
def uniq(inpt):
|
def uniq(inpt):
|
||||||
output = []
|
output = []
|
||||||
|
inpt = [ " ".join(inp.split()) for inp in inpt]
|
||||||
for x in inpt:
|
for x in inpt:
|
||||||
if x not in output:
|
if x not in output:
|
||||||
output.append(x)
|
output.append(x)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def check_email(email):
|
||||||
|
email = valid_email(email)
|
||||||
|
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
|
||||||
|
log.error(u"Found an existing account for this e-mail address")
|
||||||
|
raise Exception(_(u"Found an existing account for this e-mail address"))
|
||||||
|
return email
|
||||||
|
|
||||||
|
|
||||||
|
def check_username(username):
|
||||||
|
username = username.strip()
|
||||||
|
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
|
||||||
|
log.error(u"This username is already taken")
|
||||||
|
raise Exception (_(u"This username is already taken"))
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
def valid_email(email):
|
||||||
|
email = email.strip()
|
||||||
|
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
|
||||||
|
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
|
||||||
|
email):
|
||||||
|
log.error(u"Invalid e-mail address format")
|
||||||
|
raise Exception(_(u"Invalid e-mail address format"))
|
||||||
|
return email
|
||||||
|
|
||||||
# ################################# External interface #################################
|
# ################################# External interface #################################
|
||||||
|
|
||||||
|
|
||||||
@@ -561,8 +572,8 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
|||||||
else:
|
else:
|
||||||
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
else:
|
else:
|
||||||
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
||||||
@@ -591,10 +602,6 @@ def save_cover_from_url(url, book_path):
|
|||||||
|
|
||||||
|
|
||||||
def save_cover_from_filestorage(filepath, saved_filename, img):
|
def save_cover_from_filestorage(filepath, saved_filename, img):
|
||||||
if hasattr(img,"metadata"):
|
|
||||||
img.save(filename=os.path.join(filepath, saved_filename))
|
|
||||||
img.close()
|
|
||||||
else:
|
|
||||||
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
|
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
try:
|
try:
|
||||||
@@ -603,6 +610,17 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
|||||||
log.error(u"Failed to create path for cover")
|
log.error(u"Failed to create path for cover")
|
||||||
return False, _(u"Failed to create path for cover")
|
return False, _(u"Failed to create path for cover")
|
||||||
try:
|
try:
|
||||||
|
# upload of jgp file without wand
|
||||||
|
if isinstance(img, requests.Response):
|
||||||
|
with open(os.path.join(filepath, saved_filename), 'wb') as f:
|
||||||
|
f.write(img.content)
|
||||||
|
else:
|
||||||
|
if hasattr(img, "metadata"):
|
||||||
|
# upload of jpg/png... via url
|
||||||
|
img.save(filename=os.path.join(filepath, saved_filename))
|
||||||
|
img.close()
|
||||||
|
else:
|
||||||
|
# upload of jpg/png... from hdd
|
||||||
img.save(os.path.join(filepath, saved_filename))
|
img.save(os.path.join(filepath, saved_filename))
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
log.error(u"Cover-file is not a valid image file, or could not be stored")
|
log.error(u"Cover-file is not a valid image file, or could not be stored")
|
||||||
@@ -671,6 +689,7 @@ def do_download_file(book, book_format, client, data, headers):
|
|||||||
# ToDo Check headers parameter
|
# ToDo Check headers parameter
|
||||||
for element in headers:
|
for element in headers:
|
||||||
response.headers[element[0]] = element[1]
|
response.headers[element[0]] = element[1]
|
||||||
|
log.info('Downloading file: {}'.format(os.path.join(filename, data.name + "." + book_format)))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
@@ -684,15 +703,12 @@ def check_unrar(unrarLocation):
|
|||||||
return _('Unrar binary file not found')
|
return _('Unrar binary file not found')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
unrarLocation = unrarLocation.encode(sys.getfilesystemencoding())
|
|
||||||
unrarLocation = [unrarLocation]
|
unrarLocation = [unrarLocation]
|
||||||
for lines in process_wait(unrarLocation):
|
value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware')
|
||||||
value = re.search('UNRAR (.*) freeware', lines, re.IGNORECASE)
|
|
||||||
if value:
|
if value:
|
||||||
version = value.group(1)
|
version = value.group(1)
|
||||||
log.debug("unrar version %s", version)
|
log.debug("unrar version %s", version)
|
||||||
break
|
|
||||||
except (OSError, UnicodeDecodeError) as err:
|
except (OSError, UnicodeDecodeError) as err:
|
||||||
log.debug_or_exception(err)
|
log.debug_or_exception(err)
|
||||||
return _('Error excecuting UnRar')
|
return _('Error excecuting UnRar')
|
||||||
@@ -710,7 +726,6 @@ def json_serial(obj):
|
|||||||
'seconds': obj.seconds,
|
'seconds': obj.seconds,
|
||||||
'microseconds': obj.microseconds,
|
'microseconds': obj.microseconds,
|
||||||
}
|
}
|
||||||
# return obj.isoformat()
|
|
||||||
raise TypeError("Type %s not serializable" % type(obj))
|
raise TypeError("Type %s not serializable" % type(obj))
|
||||||
|
|
||||||
|
|
||||||
@@ -734,8 +749,8 @@ def format_runtime(runtime):
|
|||||||
# helper function to apply localize status information in tasklist entries
|
# helper function to apply localize status information in tasklist entries
|
||||||
def render_task_status(tasklist):
|
def render_task_status(tasklist):
|
||||||
renderedtasklist = list()
|
renderedtasklist = list()
|
||||||
for num, user, added, task in tasklist:
|
for __, user, __, task in tasklist:
|
||||||
if user == current_user.nickname or current_user.role_admin():
|
if user == current_user.name or current_user.role_admin():
|
||||||
ret = {}
|
ret = {}
|
||||||
if task.start_time:
|
if task.start_time:
|
||||||
ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())
|
ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())
|
||||||
@@ -756,7 +771,7 @@ def render_task_status(tasklist):
|
|||||||
|
|
||||||
ret['taskMessage'] = "{}: {}".format(_(task.name), task.message)
|
ret['taskMessage'] = "{}: {}".format(_(task.name), task.message)
|
||||||
ret['progress'] = "{} %".format(int(task.progress * 100))
|
ret['progress'] = "{} %".format(int(task.progress * 100))
|
||||||
ret['user'] = user
|
ret['user'] = escape(user) # prevent xss
|
||||||
renderedtasklist.append(ret)
|
renderedtasklist.append(ret)
|
||||||
|
|
||||||
return renderedtasklist
|
return renderedtasklist
|
||||||
@@ -773,8 +788,8 @@ def tags_filters():
|
|||||||
# checks if domain is in database (including wildcards)
|
# checks if domain is in database (including wildcards)
|
||||||
# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
|
# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
|
||||||
# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
|
# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
|
||||||
|
# in all calls the email address is checked for validity
|
||||||
def check_valid_domain(domain_text):
|
def check_valid_domain(domain_text):
|
||||||
# domain_text = domain_text.split('@', 1)[-1].lower()
|
|
||||||
sql = "SELECT * FROM registration WHERE (:domain LIKE domain and allow = 1);"
|
sql = "SELECT * FROM registration WHERE (:domain LIKE domain and allow = 1);"
|
||||||
result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all()
|
result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all()
|
||||||
if not len(result):
|
if not len(result):
|
||||||
@@ -808,6 +823,7 @@ def get_download_link(book_id, book_format, client):
|
|||||||
if book:
|
if book:
|
||||||
data1 = calibre_db.get_book_format(book.id, book_format.upper())
|
data1 = calibre_db.get_book_format(book.id, book_format.upper())
|
||||||
else:
|
else:
|
||||||
|
log.error("Book id {} not found for downloading".format(book_id))
|
||||||
abort(404)
|
abort(404)
|
||||||
if data1:
|
if data1:
|
||||||
# collect downloaded books only for registered user and not for anonymous user
|
# collect downloaded books only for registered user and not for anonymous user
|
||||||
@@ -815,8 +831,8 @@ def get_download_link(book_id, book_format, client):
|
|||||||
ub.update_download(book_id, int(current_user.id))
|
ub.update_download(book_id, int(current_user.id))
|
||||||
file_name = book.title
|
file_name = book.title
|
||||||
if len(book.authors) > 0:
|
if len(book.authors) > 0:
|
||||||
file_name = book.authors[0].name + '_' + file_name
|
file_name = file_name + ' - ' + book.authors[0].name
|
||||||
file_name = get_valid_filename(file_name)
|
file_name = get_valid_filename(file_name, replace_whitespace=False)
|
||||||
headers = Headers()
|
headers = Headers()
|
||||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||||
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
||||||
|
|||||||
+11
-10
@@ -16,8 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
|
|
||||||
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
|
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
|
||||||
|
|
||||||
|
|
||||||
@@ -57,27 +55,30 @@ def get_language_name(locale, lang_code):
|
|||||||
|
|
||||||
def get_language_codes(locale, language_names, remainder=None):
|
def get_language_codes(locale, language_names, remainder=None):
|
||||||
language_names = set(x.strip().lower() for x in language_names if x)
|
language_names = set(x.strip().lower() for x in language_names if x)
|
||||||
languages = list()
|
lang = list()
|
||||||
for k, v in get_language_names(locale).items():
|
for k, v in get_language_names(locale).items():
|
||||||
v = v.lower()
|
v = v.lower()
|
||||||
if v in language_names:
|
if v in language_names:
|
||||||
languages.append(k)
|
lang.append(k)
|
||||||
language_names.remove(v)
|
language_names.remove(v)
|
||||||
if remainder is not None:
|
if remainder is not None and language_names:
|
||||||
remainder.extend(language_names)
|
remainder.extend(language_names)
|
||||||
return languages
|
return lang
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_valid_language_codes(locale, language_names, remainder=None):
|
def get_valid_language_codes(locale, language_names, remainder=None):
|
||||||
languages = list()
|
lang = list()
|
||||||
if "" in language_names:
|
if "" in language_names:
|
||||||
language_names.remove("")
|
language_names.remove("")
|
||||||
for k, v in get_language_names(locale).items():
|
for k, __ in get_language_names(locale).items():
|
||||||
if k in language_names:
|
if k in language_names:
|
||||||
languages.append(k)
|
lang.append(k)
|
||||||
language_names.remove(k)
|
language_names.remove(k)
|
||||||
if remainder is not None and len(language_names):
|
if remainder is not None and len(language_names):
|
||||||
remainder.extend(language_names)
|
remainder.extend(language_names)
|
||||||
return languages
|
return lang
|
||||||
|
|
||||||
|
|
||||||
def get_lang3(lang):
|
def get_lang3(lang):
|
||||||
try:
|
try:
|
||||||
|
|||||||
+1165
-419
File diff suppressed because it is too large
Load Diff
+11
-7
@@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
# custom jinja filters
|
# custom jinja filters
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import datetime
|
import datetime
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -31,7 +30,7 @@ from babel.dates import format_date
|
|||||||
from flask import Blueprint, request, url_for
|
from flask import Blueprint, request, url_for
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from markupsafe import escape
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ def formatdate_filter(val):
|
|||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
|
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
|
||||||
current_user.locale,
|
current_user.locale,
|
||||||
current_user.nickname
|
current_user.name
|
||||||
)
|
)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@@ -113,21 +112,26 @@ def yesno(value, yes, no):
|
|||||||
|
|
||||||
@jinjia.app_template_filter('formatfloat')
|
@jinjia.app_template_filter('formatfloat')
|
||||||
def formatfloat(value, decimals=1):
|
def formatfloat(value, decimals=1):
|
||||||
formatedstring = '%d' % value
|
value = 0 if not value else value
|
||||||
if (value % 1) != 0:
|
return ('{0:.' + str(decimals) + 'f}').format(value).rstrip('0').rstrip('.')
|
||||||
formatedstring = ('%s.%d' % (formatedstring, (value % 1) * 10**decimals)).rstrip('0')
|
|
||||||
return formatedstring
|
|
||||||
|
|
||||||
|
|
||||||
@jinjia.app_template_filter('formatseriesindex')
|
@jinjia.app_template_filter('formatseriesindex')
|
||||||
def formatseriesindex_filter(series_index):
|
def formatseriesindex_filter(series_index):
|
||||||
if series_index:
|
if series_index:
|
||||||
|
try:
|
||||||
if int(series_index) - series_index == 0:
|
if int(series_index) - series_index == 0:
|
||||||
return int(series_index)
|
return int(series_index)
|
||||||
else:
|
else:
|
||||||
return series_index
|
return series_index
|
||||||
|
except ValueError:
|
||||||
|
return series_index
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@jinjia.app_template_filter('escapedlink')
|
||||||
|
def escapedlink_filter(url, text):
|
||||||
|
return "<a href='{}'>{}</a>".format(url, escape(text))
|
||||||
|
|
||||||
@jinjia.app_template_filter('uuidfilter')
|
@jinjia.app_template_filter('uuidfilter')
|
||||||
def uuidfilter(var):
|
def uuidfilter(var):
|
||||||
return uuid4()
|
return uuid4()
|
||||||
|
|||||||
+170
-71
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
@@ -43,11 +42,13 @@ from flask_login import current_user
|
|||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
from sqlalchemy.orm import load_only
|
|
||||||
from sqlalchemy.exc import StatementError
|
from sqlalchemy.exc import StatementError
|
||||||
|
from sqlalchemy.sql import select
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub
|
|
||||||
|
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||||
|
from .constants import sqlalchemy_version2
|
||||||
from .helper import get_download_link
|
from .helper import get_download_link
|
||||||
from .services import SyncToken as SyncToken
|
from .services import SyncToken as SyncToken
|
||||||
from .web import download_required
|
from .web import download_required
|
||||||
@@ -82,6 +83,7 @@ CONNECTION_SPECIFIC_HEADERS = [
|
|||||||
"transfer-encoding",
|
"transfer-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_kobo_activated():
|
def get_kobo_activated():
|
||||||
return config.config_kobo_sync
|
return config.config_kobo_sync
|
||||||
|
|
||||||
@@ -136,6 +138,7 @@ def convert_to_kobo_timestamp_string(timestamp):
|
|||||||
def HandleSyncRequest():
|
def HandleSyncRequest():
|
||||||
sync_token = SyncToken.SyncToken.from_headers(request.headers)
|
sync_token = SyncToken.SyncToken.from_headers(request.headers)
|
||||||
log.info("Kobo library sync request received.")
|
log.info("Kobo library sync request received.")
|
||||||
|
log.debug("SyncToken: {}".format(sync_token))
|
||||||
if not current_app.wsgi_app.is_proxied:
|
if not current_app.wsgi_app.is_proxied:
|
||||||
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
||||||
|
|
||||||
@@ -152,30 +155,65 @@ def HandleSyncRequest():
|
|||||||
# in case of external changes (e.g: adding a book through Calibre).
|
# in case of external changes (e.g: adding a book through Calibre).
|
||||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||||
|
|
||||||
if sync_token.books_last_id > -1:
|
only_kobo_shelves = current_user.kobo_only_shelves_sync
|
||||||
changed_entries = (
|
|
||||||
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
if only_kobo_shelves:
|
||||||
|
if sqlalchemy_version2:
|
||||||
|
changed_entries = select(db.Books,
|
||||||
|
ub.ArchivedBook.last_modified,
|
||||||
|
ub.BookShelf.date_added,
|
||||||
|
ub.ArchivedBook.is_archived)
|
||||||
|
else:
|
||||||
|
changed_entries = calibre_db.session.query(db.Books,
|
||||||
|
ub.ArchivedBook.last_modified,
|
||||||
|
ub.BookShelf.date_added,
|
||||||
|
ub.ArchivedBook.is_archived)
|
||||||
|
changed_entries = (changed_entries
|
||||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||||
.filter(db.Books.last_modified >= sync_token.books_last_modified)
|
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
|
||||||
.filter(db.Books.id>sync_token.books_last_id)
|
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
|
||||||
|
ub.KoboSyncedBooks.book_id == None))
|
||||||
|
.filter(ub.BookShelf.date_added > sync_token.books_last_modified)
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
.order_by(db.Books.last_modified)
|
.filter(calibre_db.common_filters(allow_show_archived=True))
|
||||||
.order_by(db.Books.id)
|
.order_by(db.Books.id)
|
||||||
.limit(SYNC_ITEM_LIMIT)
|
.order_by(ub.ArchivedBook.last_modified)
|
||||||
|
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
|
||||||
|
.join(ub.Shelf)
|
||||||
|
.filter(ub.Shelf.user_id == current_user.id)
|
||||||
|
.filter(ub.Shelf.kobo_sync)
|
||||||
|
.distinct()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
changed_entries = (
|
if sqlalchemy_version2:
|
||||||
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||||
|
else:
|
||||||
|
changed_entries = calibre_db.session.query(db.Books,
|
||||||
|
ub.ArchivedBook.last_modified,
|
||||||
|
ub.ArchivedBook.is_archived)
|
||||||
|
changed_entries = (changed_entries
|
||||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||||
.filter(db.Books.last_modified > sync_token.books_last_modified)
|
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
|
||||||
|
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
|
||||||
|
ub.KoboSyncedBooks.book_id == None))
|
||||||
|
.filter(calibre_db.common_filters())
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
.order_by(db.Books.last_modified)
|
.order_by(db.Books.last_modified)
|
||||||
.order_by(db.Books.id)
|
.order_by(db.Books.id)
|
||||||
.limit(SYNC_ITEM_LIMIT)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
reading_states_in_new_entitlements = []
|
reading_states_in_new_entitlements = []
|
||||||
for book in changed_entries:
|
if sqlalchemy_version2:
|
||||||
|
books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
|
||||||
|
else:
|
||||||
|
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
||||||
|
for book in books:
|
||||||
|
kobo_sync_status.add_synced_books(book.Books.id)
|
||||||
|
formats = [data.format for data in book.Books.data]
|
||||||
|
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||||
|
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||||
|
|
||||||
kobo_reading_state = get_or_create_reading_state(book.Books.id)
|
kobo_reading_state = get_or_create_reading_state(book.Books.id)
|
||||||
entitlement = {
|
entitlement = {
|
||||||
"BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
|
"BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
|
||||||
@@ -187,7 +225,14 @@ def HandleSyncRequest():
|
|||||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||||
reading_states_in_new_entitlements.append(book.Books.id)
|
reading_states_in_new_entitlements.append(book.Books.id)
|
||||||
|
|
||||||
if book.Books.timestamp > sync_token.books_last_created:
|
ts_created = book.Books.timestamp
|
||||||
|
|
||||||
|
try:
|
||||||
|
ts_created = max(ts_created, book.date_added)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if ts_created > sync_token.books_last_created:
|
||||||
sync_results.append({"NewEntitlement": entitlement})
|
sync_results.append({"NewEntitlement": entitlement})
|
||||||
else:
|
else:
|
||||||
sync_results.append({"ChangedEntitlement": entitlement})
|
sync_results.append({"ChangedEntitlement": entitlement})
|
||||||
@@ -195,36 +240,59 @@ def HandleSyncRequest():
|
|||||||
new_books_last_modified = max(
|
new_books_last_modified = max(
|
||||||
book.Books.last_modified, new_books_last_modified
|
book.Books.last_modified, new_books_last_modified
|
||||||
)
|
)
|
||||||
new_books_last_created = max(book.Books.timestamp, new_books_last_created)
|
try:
|
||||||
|
new_books_last_modified = max(
|
||||||
max_change = (changed_entries
|
new_books_last_modified, book.date_added
|
||||||
.from_self()
|
|
||||||
.filter(ub.ArchivedBook.is_archived)
|
|
||||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc())
|
|
||||||
.first()
|
|
||||||
)
|
)
|
||||||
if max_change:
|
except AttributeError:
|
||||||
max_change = max_change.last_modified
|
pass
|
||||||
|
|
||||||
|
new_books_last_created = max(ts_created, new_books_last_created)
|
||||||
|
|
||||||
|
if sqlalchemy_version2:
|
||||||
|
max_change = calibre_db.session.execute(changed_entries
|
||||||
|
.filter(ub.ArchivedBook.is_archived)
|
||||||
|
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
|
||||||
|
.columns(db.Books).first()
|
||||||
else:
|
else:
|
||||||
max_change = new_archived_last_modified
|
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived) \
|
||||||
|
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
|
||||||
|
|
||||||
|
max_change = max_change.last_modified if max_change else new_archived_last_modified
|
||||||
|
|
||||||
new_archived_last_modified = max(new_archived_last_modified, max_change)
|
new_archived_last_modified = max(new_archived_last_modified, max_change)
|
||||||
|
|
||||||
# no. of books returned
|
# no. of books returned
|
||||||
book_count = changed_entries.count()
|
if sqlalchemy_version2:
|
||||||
|
entries = calibre_db.session.execute(changed_entries).all()
|
||||||
# last entry:
|
book_count = len(entries)
|
||||||
if book_count:
|
|
||||||
books_last_id = changed_entries.all()[-1].Books.id or -1
|
|
||||||
else:
|
else:
|
||||||
books_last_id = -1
|
book_count = changed_entries.count()
|
||||||
|
# last entry:
|
||||||
|
cont_sync = bool(book_count)
|
||||||
|
log.debug("Remaining books to Sync: {}".format(book_count))
|
||||||
# generate reading state data
|
# generate reading state data
|
||||||
changed_reading_states = (
|
changed_reading_states = ub.session.query(ub.KoboReadingState)
|
||||||
ub.session.query(ub.KoboReadingState)
|
|
||||||
.filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
|
if only_kobo_shelves:
|
||||||
ub.KoboReadingState.user_id == current_user.id,
|
changed_reading_states = changed_reading_states.join(ub.BookShelf,
|
||||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))))
|
ub.KoboReadingState.book_id == ub.BookShelf.book_id)\
|
||||||
for kobo_reading_state in changed_reading_states.all():
|
.join(ub.Shelf)\
|
||||||
|
.filter(current_user.id == ub.Shelf.user_id)\
|
||||||
|
.filter(ub.Shelf.kobo_sync,
|
||||||
|
or_(
|
||||||
|
ub.KoboReadingState.last_modified > sync_token.reading_state_last_modified,
|
||||||
|
func.datetime(ub.BookShelf.date_added) > sync_token.books_last_modified
|
||||||
|
)).distinct()
|
||||||
|
else:
|
||||||
|
changed_reading_states = changed_reading_states.filter(
|
||||||
|
ub.KoboReadingState.last_modified > sync_token.reading_state_last_modified)
|
||||||
|
|
||||||
|
changed_reading_states = changed_reading_states.filter(
|
||||||
|
and_(ub.KoboReadingState.user_id == current_user.id,
|
||||||
|
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))
|
||||||
|
cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT)
|
||||||
|
for kobo_reading_state in changed_reading_states.limit(SYNC_ITEM_LIMIT).all():
|
||||||
book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
|
book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
|
||||||
if book:
|
if book:
|
||||||
sync_results.append({
|
sync_results.append({
|
||||||
@@ -234,15 +302,15 @@ def HandleSyncRequest():
|
|||||||
})
|
})
|
||||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||||
|
|
||||||
sync_shelves(sync_token, sync_results)
|
sync_shelves(sync_token, sync_results, only_kobo_shelves)
|
||||||
|
|
||||||
sync_token.books_last_created = new_books_last_created
|
sync_token.books_last_created = new_books_last_created
|
||||||
sync_token.books_last_modified = new_books_last_modified
|
sync_token.books_last_modified = new_books_last_modified
|
||||||
sync_token.archive_last_modified = new_archived_last_modified
|
sync_token.archive_last_modified = new_archived_last_modified
|
||||||
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
||||||
sync_token.books_last_id = books_last_id
|
# sync_token.books_last_id = books_last_id
|
||||||
|
|
||||||
return generate_sync_response(sync_token, sync_results, book_count)
|
return generate_sync_response(sync_token, sync_results, cont_sync)
|
||||||
|
|
||||||
|
|
||||||
def generate_sync_response(sync_token, sync_results, set_cont=False):
|
def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||||
@@ -259,12 +327,13 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
|||||||
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
|
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
|
||||||
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
|
log.error("Failed to receive or parse response from Kobo's sync endpoint: {}".format(ex))
|
||||||
if set_cont:
|
if set_cont:
|
||||||
extra_headers["x-kobo-sync"] = "continue"
|
extra_headers["x-kobo-sync"] = "continue"
|
||||||
sync_token.to_headers(extra_headers)
|
sync_token.to_headers(extra_headers)
|
||||||
|
|
||||||
|
# log.debug("Kobo Sync Content: {}".format(sync_results))
|
||||||
response = make_response(jsonify(sync_results), extra_headers)
|
response = make_response(jsonify(sync_results), extra_headers)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -302,7 +371,8 @@ def get_download_url_for_book(book, book_format):
|
|||||||
book_format=book_format.lower()
|
book_format=book_format.lower()
|
||||||
)
|
)
|
||||||
return url_for(
|
return url_for(
|
||||||
"web.download_link",
|
"kobo.download_book",
|
||||||
|
auth_token=kobo_auth.get_auth_token(),
|
||||||
book_id=book.id,
|
book_id=book.id,
|
||||||
book_format=book_format.lower(),
|
book_format=book_format.lower(),
|
||||||
_external=True,
|
_external=True,
|
||||||
@@ -416,9 +486,6 @@ def get_metadata(book):
|
|||||||
metadata.update(get_author(book))
|
metadata.update(get_author(book))
|
||||||
|
|
||||||
if get_series(book):
|
if get_series(book):
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
name = get_series(book).encode("utf-8")
|
|
||||||
else:
|
|
||||||
name = get_series(book)
|
name = get_series(book)
|
||||||
metadata["Series"] = {
|
metadata["Series"] = {
|
||||||
"Name": get_series(book),
|
"Name": get_series(book),
|
||||||
@@ -430,7 +497,7 @@ def get_metadata(book):
|
|||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||||
@@ -465,6 +532,7 @@ def HandleTagCreate():
|
|||||||
return make_response(jsonify(str(shelf.uuid)), 201)
|
return make_response(jsonify(str(shelf.uuid)), 201)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleTagUpdate(tag_id):
|
def HandleTagUpdate(tag_id):
|
||||||
@@ -520,6 +588,7 @@ def add_items_to_shelf(items, shelf):
|
|||||||
return items_unknown_to_calibre
|
return items_unknown_to_calibre
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleTagAddItem(tag_id):
|
def HandleTagAddItem(tag_id):
|
||||||
@@ -549,6 +618,7 @@ def HandleTagAddItem(tag_id):
|
|||||||
return make_response('', 201)
|
return make_response('', 201)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleTagRemoveItem(tag_id):
|
def HandleTagRemoveItem(tag_id):
|
||||||
@@ -595,13 +665,14 @@ def HandleTagRemoveItem(tag_id):
|
|||||||
|
|
||||||
# Add new, changed, or deleted shelves to the sync_results.
|
# Add new, changed, or deleted shelves to the sync_results.
|
||||||
# Note: Public shelves that aren't owned by the user aren't supported.
|
# Note: Public shelves that aren't owned by the user aren't supported.
|
||||||
def sync_shelves(sync_token, sync_results):
|
def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
||||||
new_tags_last_modified = sync_token.tags_last_modified
|
new_tags_last_modified = sync_token.tags_last_modified
|
||||||
|
|
||||||
for shelf in ub.session.query(ub.ShelfArchive).filter(func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified,
|
for shelf in ub.session.query(ub.ShelfArchive).filter(
|
||||||
ub.ShelfArchive.user_id == current_user.id):
|
func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified,
|
||||||
|
ub.ShelfArchive.user_id == current_user.id
|
||||||
|
):
|
||||||
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
|
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
|
||||||
|
|
||||||
sync_results.append({
|
sync_results.append({
|
||||||
"DeletedTag": {
|
"DeletedTag": {
|
||||||
"Tag": {
|
"Tag": {
|
||||||
@@ -610,9 +681,44 @@ def sync_shelves(sync_token, sync_results):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
ub.session.delete(shelf)
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
for shelf in ub.session.query(ub.Shelf).filter(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
|
||||||
ub.Shelf.user_id == current_user.id):
|
extra_filters = []
|
||||||
|
if only_kobo_shelves:
|
||||||
|
for shelf in ub.session.query(ub.Shelf).filter(
|
||||||
|
func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||||
|
ub.Shelf.user_id == current_user.id,
|
||||||
|
not ub.Shelf.kobo_sync
|
||||||
|
):
|
||||||
|
sync_results.append({
|
||||||
|
"DeletedTag": {
|
||||||
|
"Tag": {
|
||||||
|
"Id": shelf.uuid,
|
||||||
|
"LastModified": convert_to_kobo_timestamp_string(shelf.last_modified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
extra_filters.append(ub.Shelf.kobo_sync)
|
||||||
|
|
||||||
|
if sqlalchemy_version2:
|
||||||
|
shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
||||||
|
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||||
|
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
||||||
|
ub.Shelf.user_id == current_user.id,
|
||||||
|
*extra_filters
|
||||||
|
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf)
|
||||||
|
else:
|
||||||
|
shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
||||||
|
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||||
|
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
||||||
|
ub.Shelf.user_id == current_user.id,
|
||||||
|
*extra_filters
|
||||||
|
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())
|
||||||
|
|
||||||
|
|
||||||
|
for shelf in shelflist:
|
||||||
if not shelf_lib.check_shelf_view_permissions(shelf):
|
if not shelf_lib.check_shelf_view_permissions(shelf):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -657,7 +763,7 @@ def create_kobo_tag(shelf):
|
|||||||
)
|
)
|
||||||
return {"Tag": tag}
|
return {"Tag": tag}
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleStateRequest(book_uuid):
|
def HandleStateRequest(book_uuid):
|
||||||
@@ -832,6 +938,7 @@ def TopLevelEndpoint():
|
|||||||
return make_response(jsonify({}))
|
return make_response(jsonify({}))
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleBookDeletionRequest(book_uuid):
|
def HandleBookDeletionRequest(book_uuid):
|
||||||
@@ -854,10 +961,11 @@ def HandleBookDeletionRequest(book_uuid):
|
|||||||
|
|
||||||
ub.session.merge(archived_book)
|
ub.session.merge(archived_book)
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
return ("", 204)
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
# TODO: Implement the following routes
|
# TODO: Implement the following routes
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||||
def HandleUnimplementedRequest(dummy=None):
|
def HandleUnimplementedRequest(dummy=None):
|
||||||
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
||||||
@@ -865,6 +973,7 @@ def HandleUnimplementedRequest(dummy=None):
|
|||||||
|
|
||||||
|
|
||||||
# TODO: Implement the following routes
|
# TODO: Implement the following routes
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
||||||
@@ -875,6 +984,7 @@ def HandleUserRequest(dummy=None):
|
|||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||||
@@ -889,17 +999,6 @@ def HandleProductsRequest(dummy=None):
|
|||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
'''@kobo.errorhandler(404)
|
|
||||||
def handle_404(err):
|
|
||||||
# This handler acts as a catch-all for endpoints that we don't have an interest in
|
|
||||||
# implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc)
|
|
||||||
if err:
|
|
||||||
print('404')
|
|
||||||
return jsonify(error=str(err)), 404
|
|
||||||
log.debug("Unknown Request received: %s, method: %s, data: %s", request.base_url, request.method, request.data)
|
|
||||||
return redirect_or_proxy_request()'''
|
|
||||||
|
|
||||||
|
|
||||||
def make_calibre_web_auth_response():
|
def make_calibre_web_auth_response():
|
||||||
# As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for
|
# As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for
|
||||||
# authentation (nor for authorization). We return a dummy response just to keep the device happy.
|
# authentation (nor for authorization). We return a dummy response just to keep the device happy.
|
||||||
@@ -919,6 +1018,7 @@ def make_calibre_web_auth_response():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleAuthRequest():
|
def HandleAuthRequest():
|
||||||
@@ -926,7 +1026,7 @@ def HandleAuthRequest():
|
|||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
try:
|
try:
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
except:
|
except Exception:
|
||||||
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
|
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
|
||||||
return make_calibre_web_auth_response()
|
return make_calibre_web_auth_response()
|
||||||
|
|
||||||
@@ -943,7 +1043,7 @@ def HandleInitRequest():
|
|||||||
store_response_json = store_response.json()
|
store_response_json = store_response.json()
|
||||||
if "Resources" in store_response_json:
|
if "Resources" in store_response_json:
|
||||||
kobo_resources = store_response_json["Resources"]
|
kobo_resources = store_response_json["Resources"]
|
||||||
except:
|
except Exception:
|
||||||
log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.")
|
log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.")
|
||||||
if not kobo_resources:
|
if not kobo_resources:
|
||||||
kobo_resources = NATIVE_KOBO_RESOURCES()
|
kobo_resources = NATIVE_KOBO_RESOURCES()
|
||||||
@@ -1004,7 +1104,6 @@ def HandleInitRequest():
|
|||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
@download_required
|
@download_required
|
||||||
def download_book(book_id, book_format):
|
def download_book(book_id, book_format):
|
||||||
|
|
||||||
return get_download_link(book_id, book_format, "kobo")
|
return get_download_link(book_id, book_format, "kobo")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -64,10 +64,10 @@ from datetime import datetime
|
|||||||
from os import urandom
|
from os import urandom
|
||||||
|
|
||||||
from flask import g, Blueprint, url_for, abort, request
|
from flask import g, Blueprint, url_for, abort, request
|
||||||
from flask_login import login_user, login_required
|
from flask_login import login_user, current_user, login_required
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from . import logger, ub, lm
|
from . import logger, config, calibre_db, db, helper, ub, lm
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -81,6 +81,7 @@ log = logger.create()
|
|||||||
|
|
||||||
def register_url_value_preprocessor(kobo):
|
def register_url_value_preprocessor(kobo):
|
||||||
@kobo.url_value_preprocessor
|
@kobo.url_value_preprocessor
|
||||||
|
# pylint: disable=unused-variable
|
||||||
def pop_auth_token(__, values):
|
def pop_auth_token(__, values):
|
||||||
g.auth_token = values.pop("auth_token")
|
g.auth_token = values.pop("auth_token")
|
||||||
|
|
||||||
@@ -148,6 +149,14 @@ def generate_auth_token(user_id):
|
|||||||
|
|
||||||
ub.session.add(auth_token)
|
ub.session.add(auth_token)
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
|
|
||||||
|
books = calibre_db.session.query(db.Books).join(db.Data).all()
|
||||||
|
|
||||||
|
for book in books:
|
||||||
|
formats = [data.format for data in book.data]
|
||||||
|
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||||
|
helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||||
|
|
||||||
return render_title_template(
|
return render_title_template(
|
||||||
"generate_kobo_auth_url.html",
|
"generate_kobo_auth_url.html",
|
||||||
title=_(u"Kobo Setup"),
|
title=_(u"Kobo Setup"),
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from flask_login import current_user
|
||||||
|
from . import ub
|
||||||
|
import datetime
|
||||||
|
from sqlalchemy.sql.expression import or_
|
||||||
|
|
||||||
|
|
||||||
|
def add_synced_books(book_id):
|
||||||
|
synced_book = ub.KoboSyncedBooks()
|
||||||
|
synced_book.user_id = current_user.id
|
||||||
|
synced_book.book_id = book_id
|
||||||
|
ub.session.add(synced_book)
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_synced_book(book_id):
|
||||||
|
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).delete()
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
def add_archived_books(book_id):
|
||||||
|
archived_book = (
|
||||||
|
ub.session.query(ub.ArchivedBook)
|
||||||
|
.filter(ub.ArchivedBook.book_id == book_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not archived_book:
|
||||||
|
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
||||||
|
archived_book.is_archived = True
|
||||||
|
archived_book.last_modified = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
ub.session.merge(archived_book)
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# select all books which are synced by the current user and do not belong to a synced shelf and them to archive
|
||||||
|
# select all shelfs from current user which are synced and do not belong to the "only sync" shelfs
|
||||||
|
def update_on_sync_shelfs(content_id):
|
||||||
|
books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
|
||||||
|
.join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True)
|
||||||
|
.join(ub.Shelf, ub.Shelf.user_id == content_id, isouter=True)
|
||||||
|
.filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None))
|
||||||
|
.filter(ub.KoboSyncedBooks.user_id == content_id).all())
|
||||||
|
for b in books_to_archive:
|
||||||
|
add_archived_books(b.book_id)
|
||||||
|
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == b.book_id).filter(ub.KoboSyncedBooks.user_id == content_id).delete()
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
shelfs_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == content_id).filter(
|
||||||
|
ub.Shelf.kobo_sync == 0).all()
|
||||||
|
for a in shelfs_to_archive:
|
||||||
|
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=content_id))
|
||||||
|
ub.session_commit()
|
||||||
+5
-6
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
@@ -62,11 +61,11 @@ class _Logger(logging.Logger):
|
|||||||
|
|
||||||
|
|
||||||
def debug_no_auth(self, message, *args, **kwargs):
|
def debug_no_auth(self, message, *args, **kwargs):
|
||||||
|
message = message.strip("\r\n")
|
||||||
if message.startswith("send: AUTH"):
|
if message.startswith("send: AUTH"):
|
||||||
self.debug(message[:16], stacklevel=2, *args, **kwargs)
|
self.debug(message[:16], *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
self.debug(message, stacklevel=2, *args, **kwargs)
|
self.debug(message, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get(name=None):
|
def get(name=None):
|
||||||
@@ -153,11 +152,11 @@ def setup(log_file, log_level=None):
|
|||||||
file_handler.baseFilename = log_file
|
file_handler.baseFilename = log_file
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
|
file_handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=2, encoding='utf-8')
|
||||||
except IOError:
|
except IOError:
|
||||||
if log_file == DEFAULT_LOG_FILE:
|
if log_file == DEFAULT_LOG_FILE:
|
||||||
raise
|
raise
|
||||||
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2, encoding='utf-8')
|
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=100000, backupCount=2, encoding='utf-8')
|
||||||
log_file = ""
|
log_file = ""
|
||||||
file_handler.setFormatter(FORMATTER)
|
file_handler.setFormatter(FORMATTER)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ComicVine api document: https://comicvine.gamespot.com/api/documentation
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from cps.services.Metadata import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
class ComicVine(Metadata):
|
||||||
|
__name__ = "ComicVine"
|
||||||
|
__id__ = "comicvine"
|
||||||
|
|
||||||
|
def search(self, query, __):
|
||||||
|
val = list()
|
||||||
|
apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6"
|
||||||
|
if self.active:
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Not Evil Browser'
|
||||||
|
}
|
||||||
|
|
||||||
|
result = requests.get("https://comicvine.gamespot.com/api/search?api_key="
|
||||||
|
+ apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers)
|
||||||
|
for r in result.json()['results']:
|
||||||
|
seriesTitle = r['volume'].get('name', "")
|
||||||
|
if r.get('store_date'):
|
||||||
|
dateFomers = r.get('store_date')
|
||||||
|
else:
|
||||||
|
dateFomers = r.get('date_added')
|
||||||
|
v = dict()
|
||||||
|
v['id'] = r['id']
|
||||||
|
v['title'] = seriesTitle + " #" + r.get('issue_number', "0") + " - " + ( r.get('name', "") or "")
|
||||||
|
v['authors'] = r.get('authors', [])
|
||||||
|
v['description'] = r.get('description', "")
|
||||||
|
v['publisher'] = ""
|
||||||
|
v['publishedDate'] = dateFomers
|
||||||
|
v['tags'] = ["Comics", seriesTitle]
|
||||||
|
v['rating'] = 0
|
||||||
|
v['series'] = seriesTitle
|
||||||
|
v['cover'] = r['image'].get('original_url')
|
||||||
|
v['source'] = {
|
||||||
|
"id": self.__id__,
|
||||||
|
"description": "ComicVine Books",
|
||||||
|
"link": "https://comicvine.gamespot.com/"
|
||||||
|
}
|
||||||
|
v['url'] = r.get('site_detail_url', "")
|
||||||
|
val.append(v)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# Google Books api document: https://developers.google.com/books/docs/v1/using
|
||||||
|
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from cps.services.Metadata import Metadata
|
||||||
|
|
||||||
|
class Google(Metadata):
|
||||||
|
__name__ = "Google"
|
||||||
|
__id__ = "google"
|
||||||
|
|
||||||
|
def search(self, query, __):
|
||||||
|
if self.active:
|
||||||
|
val = list()
|
||||||
|
result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+"))
|
||||||
|
for r in result.json()['items']:
|
||||||
|
v = dict()
|
||||||
|
v['id'] = r['id']
|
||||||
|
v['title'] = r['volumeInfo']['title']
|
||||||
|
v['authors'] = r['volumeInfo'].get('authors', [])
|
||||||
|
v['description'] = r['volumeInfo'].get('description', "")
|
||||||
|
v['publisher'] = r['volumeInfo'].get('publisher', "")
|
||||||
|
v['publishedDate'] = r['volumeInfo'].get('publishedDate', "")
|
||||||
|
v['tags'] = r['volumeInfo'].get('categories', [])
|
||||||
|
v['rating'] = r['volumeInfo'].get('averageRating', 0)
|
||||||
|
if r['volumeInfo'].get('imageLinks'):
|
||||||
|
v['cover'] = r['volumeInfo']['imageLinks']['thumbnail'].replace("http://", "https://")
|
||||||
|
else:
|
||||||
|
v['cover'] = "/../../../static/generic_cover.jpg"
|
||||||
|
v['source'] = {
|
||||||
|
"id": self.__id__,
|
||||||
|
"description": "Google Books",
|
||||||
|
"link": "https://books.google.com/"}
|
||||||
|
v['url'] = "https://books.google.com/books?id=" + r['id']
|
||||||
|
val.append(v)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from scholarly import scholarly
|
||||||
|
|
||||||
|
from cps.services.Metadata import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
class scholar(Metadata):
|
||||||
|
__name__ = "Google Scholar"
|
||||||
|
__id__ = "googlescholar"
|
||||||
|
|
||||||
|
def search(self, query, generic_cover=""):
|
||||||
|
val = list()
|
||||||
|
if self.active:
|
||||||
|
scholar_gen = scholarly.search_pubs(' '.join(query.split('+')))
|
||||||
|
i = 0
|
||||||
|
for publication in scholar_gen:
|
||||||
|
v = dict()
|
||||||
|
v['id'] = "1234" # publication['bib'].get('title')
|
||||||
|
v['title'] = publication['bib'].get('title')
|
||||||
|
v['authors'] = publication['bib'].get('author', [])
|
||||||
|
v['description'] = publication['bib'].get('abstract', "")
|
||||||
|
v['publisher'] = publication['bib'].get('venue', "")
|
||||||
|
if publication['bib'].get('pub_year'):
|
||||||
|
v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01"
|
||||||
|
else:
|
||||||
|
v['publishedDate'] = ""
|
||||||
|
v['tags'] = ""
|
||||||
|
v['ratings'] = 0
|
||||||
|
v['series'] = ""
|
||||||
|
v['cover'] = generic_cover
|
||||||
|
v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "",
|
||||||
|
v['source'] = {
|
||||||
|
"id": self.__id__,
|
||||||
|
"description": "Google Scholar",
|
||||||
|
"link": "https://scholar.google.com/"
|
||||||
|
}
|
||||||
|
val.append(v)
|
||||||
|
i += 1
|
||||||
|
if (i >= 10):
|
||||||
|
break
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+1
-6
@@ -16,10 +16,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
from flask import session
|
from flask import session
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
|
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
@@ -34,7 +32,7 @@ except ImportError:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
|
||||||
class OAuthBackend(SQLAlchemyBackend):
|
class OAuthBackend(SQLAlchemyBackend):
|
||||||
"""
|
"""
|
||||||
Stores and retrieves OAuth tokens using a relational database through
|
Stores and retrieves OAuth tokens using a relational database through
|
||||||
@@ -162,6 +160,3 @@ try:
|
|||||||
self.cache.delete(self.make_cache_key(
|
self.cache.delete(self.make_cache_key(
|
||||||
blueprint=blueprint, user=user, user_id=user_id,
|
blueprint=blueprint, user=user, user_id=user_id,
|
||||||
))
|
))
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|||||||
+122
-101
@@ -20,7 +20,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import json
|
import json
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
@@ -30,15 +29,20 @@ from flask_babel import gettext as _
|
|||||||
from flask_dance.consumer import oauth_authorized, oauth_error
|
from flask_dance.consumer import oauth_authorized, oauth_error
|
||||||
from flask_dance.contrib.github import make_github_blueprint, github
|
from flask_dance.contrib.github import make_github_blueprint, github
|
||||||
from flask_dance.contrib.google import make_google_blueprint, google
|
from flask_dance.contrib.google import make_google_blueprint, google
|
||||||
|
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
|
||||||
from flask_login import login_user, current_user, login_required
|
from flask_login import login_user, current_user, login_required
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
from . import constants, logger, config, app, ub
|
from . import constants, logger, config, app, ub
|
||||||
|
|
||||||
|
try:
|
||||||
from .oauth import OAuthBackend, backend_resultcode
|
from .oauth import OAuthBackend, backend_resultcode
|
||||||
|
except NameError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
oauth_check = {}
|
oauth_check = {}
|
||||||
|
oauthblueprints = []
|
||||||
oauth = Blueprint('oauth', __name__)
|
oauth = Blueprint('oauth', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ def register_user_with_oauth(user=None):
|
|||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
# no found, return error
|
# no found, return error
|
||||||
return
|
return
|
||||||
ub.session_commit("User {} with OAuth for provider {} registered".format(user.nickname, oauth_key))
|
ub.session_commit("User {} with OAuth for provider {} registered".format(user.name, oauth_key))
|
||||||
|
|
||||||
|
|
||||||
def logout_oauth_user():
|
def logout_oauth_user():
|
||||||
@@ -93,85 +97,6 @@ def logout_oauth_user():
|
|||||||
session.pop(str(oauth_key) + '_oauth_user_id')
|
session.pop(str(oauth_key) + '_oauth_user_id')
|
||||||
|
|
||||||
|
|
||||||
if ub.oauth_support:
|
|
||||||
oauthblueprints = []
|
|
||||||
if not ub.session.query(ub.OAuthProvider).count():
|
|
||||||
for provider in ("github", "google"):
|
|
||||||
oauthProvider = ub.OAuthProvider()
|
|
||||||
oauthProvider.provider_name = provider
|
|
||||||
oauthProvider.active = False
|
|
||||||
ub.session.add(oauthProvider)
|
|
||||||
ub.session_commit("{} Blueprint Created".format(provider))
|
|
||||||
|
|
||||||
oauth_ids = ub.session.query(ub.OAuthProvider).all()
|
|
||||||
ele1 = dict(provider_name='github',
|
|
||||||
id=oauth_ids[0].id,
|
|
||||||
active=oauth_ids[0].active,
|
|
||||||
oauth_client_id=oauth_ids[0].oauth_client_id,
|
|
||||||
scope=None,
|
|
||||||
oauth_client_secret=oauth_ids[0].oauth_client_secret,
|
|
||||||
obtain_link='https://github.com/settings/developers')
|
|
||||||
ele2 = dict(provider_name='google',
|
|
||||||
id=oauth_ids[1].id,
|
|
||||||
active=oauth_ids[1].active,
|
|
||||||
scope=["https://www.googleapis.com/auth/userinfo.email"],
|
|
||||||
oauth_client_id=oauth_ids[1].oauth_client_id,
|
|
||||||
oauth_client_secret=oauth_ids[1].oauth_client_secret,
|
|
||||||
obtain_link='https://console.developers.google.com/apis/credentials')
|
|
||||||
oauthblueprints.append(ele1)
|
|
||||||
oauthblueprints.append(ele2)
|
|
||||||
|
|
||||||
for element in oauthblueprints:
|
|
||||||
if element['provider_name'] == 'github':
|
|
||||||
blueprint_func = make_github_blueprint
|
|
||||||
else:
|
|
||||||
blueprint_func = make_google_blueprint
|
|
||||||
blueprint = blueprint_func(
|
|
||||||
client_id=element['oauth_client_id'],
|
|
||||||
client_secret=element['oauth_client_secret'],
|
|
||||||
redirect_to="oauth."+element['provider_name']+"_login",
|
|
||||||
scope=element['scope']
|
|
||||||
)
|
|
||||||
element['blueprint'] = blueprint
|
|
||||||
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
|
|
||||||
user=current_user, user_required=True)
|
|
||||||
app.register_blueprint(blueprint, url_prefix="/login")
|
|
||||||
if element['active']:
|
|
||||||
register_oauth_blueprint(element['id'], element['provider_name'])
|
|
||||||
|
|
||||||
|
|
||||||
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
|
|
||||||
def github_logged_in(blueprint, token):
|
|
||||||
if not token:
|
|
||||||
flash(_(u"Failed to log in with GitHub."), category="error")
|
|
||||||
return False
|
|
||||||
|
|
||||||
resp = blueprint.session.get("/user")
|
|
||||||
if not resp.ok:
|
|
||||||
flash(_(u"Failed to fetch user info from GitHub."), category="error")
|
|
||||||
return False
|
|
||||||
|
|
||||||
github_info = resp.json()
|
|
||||||
github_user_id = str(github_info["id"])
|
|
||||||
return oauth_update_token(str(oauthblueprints[0]['id']), token, github_user_id)
|
|
||||||
|
|
||||||
|
|
||||||
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
|
|
||||||
def google_logged_in(blueprint, token):
|
|
||||||
if not token:
|
|
||||||
flash(_(u"Failed to log in with Google."), category="error")
|
|
||||||
return False
|
|
||||||
|
|
||||||
resp = blueprint.session.get("/oauth2/v2/userinfo")
|
|
||||||
if not resp.ok:
|
|
||||||
flash(_(u"Failed to fetch user info from Google."), category="error")
|
|
||||||
return False
|
|
||||||
|
|
||||||
google_info = resp.json()
|
|
||||||
google_user_id = str(google_info["id"])
|
|
||||||
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
|
|
||||||
|
|
||||||
|
|
||||||
def oauth_update_token(provider_id, token, provider_user_id):
|
def oauth_update_token(provider_id, token, provider_user_id):
|
||||||
session[provider_id + "_oauth_user_id"] = provider_user_id
|
session[provider_id + "_oauth_user_id"] = provider_user_id
|
||||||
session[provider_id + "_oauth_token"] = token
|
session[provider_id + "_oauth_token"] = token
|
||||||
@@ -209,8 +134,8 @@ if ub.oauth_support:
|
|||||||
# already bind with user, just login
|
# already bind with user, just login
|
||||||
if oauth_entry.user:
|
if oauth_entry.user:
|
||||||
login_user(oauth_entry.user)
|
login_user(oauth_entry.user)
|
||||||
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname)
|
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name)
|
||||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname),
|
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name),
|
||||||
category="success")
|
category="success")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
else:
|
else:
|
||||||
@@ -221,9 +146,10 @@ if ub.oauth_support:
|
|||||||
ub.session.add(oauth_entry)
|
ub.session.add(oauth_entry)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
||||||
|
log.info("Link to {} Succeeded".format(provider_name))
|
||||||
return redirect(url_for('web.profile'))
|
return redirect(url_for('web.profile'))
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
else:
|
else:
|
||||||
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
||||||
@@ -269,8 +195,9 @@ if ub.oauth_support:
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
logout_oauth_user()
|
logout_oauth_user()
|
||||||
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
||||||
except Exception as e:
|
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
|
||||||
log.debug_or_exception(e)
|
except Exception as ex:
|
||||||
|
log.debug_or_exception(ex)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
@@ -278,6 +205,91 @@ if ub.oauth_support:
|
|||||||
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
|
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
|
||||||
return redirect(url_for('web.profile'))
|
return redirect(url_for('web.profile'))
|
||||||
|
|
||||||
|
def generate_oauth_blueprints():
|
||||||
|
if not ub.session.query(ub.OAuthProvider).count():
|
||||||
|
for provider in ("github", "google"):
|
||||||
|
oauthProvider = ub.OAuthProvider()
|
||||||
|
oauthProvider.provider_name = provider
|
||||||
|
oauthProvider.active = False
|
||||||
|
ub.session.add(oauthProvider)
|
||||||
|
ub.session_commit("{} Blueprint Created".format(provider))
|
||||||
|
|
||||||
|
oauth_ids = ub.session.query(ub.OAuthProvider).all()
|
||||||
|
ele1 = dict(provider_name='github',
|
||||||
|
id=oauth_ids[0].id,
|
||||||
|
active=oauth_ids[0].active,
|
||||||
|
oauth_client_id=oauth_ids[0].oauth_client_id,
|
||||||
|
scope=None,
|
||||||
|
oauth_client_secret=oauth_ids[0].oauth_client_secret,
|
||||||
|
obtain_link='https://github.com/settings/developers')
|
||||||
|
ele2 = dict(provider_name='google',
|
||||||
|
id=oauth_ids[1].id,
|
||||||
|
active=oauth_ids[1].active,
|
||||||
|
scope=["https://www.googleapis.com/auth/userinfo.email"],
|
||||||
|
oauth_client_id=oauth_ids[1].oauth_client_id,
|
||||||
|
oauth_client_secret=oauth_ids[1].oauth_client_secret,
|
||||||
|
obtain_link='https://console.developers.google.com/apis/credentials')
|
||||||
|
oauthblueprints.append(ele1)
|
||||||
|
oauthblueprints.append(ele2)
|
||||||
|
|
||||||
|
for element in oauthblueprints:
|
||||||
|
if element['provider_name'] == 'github':
|
||||||
|
blueprint_func = make_github_blueprint
|
||||||
|
else:
|
||||||
|
blueprint_func = make_google_blueprint
|
||||||
|
blueprint = blueprint_func(
|
||||||
|
client_id=element['oauth_client_id'],
|
||||||
|
client_secret=element['oauth_client_secret'],
|
||||||
|
redirect_to="oauth."+element['provider_name']+"_login",
|
||||||
|
scope=element['scope']
|
||||||
|
)
|
||||||
|
element['blueprint'] = blueprint
|
||||||
|
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
|
||||||
|
user=current_user, user_required=True)
|
||||||
|
app.register_blueprint(blueprint, url_prefix="/login")
|
||||||
|
if element['active']:
|
||||||
|
register_oauth_blueprint(element['id'], element['provider_name'])
|
||||||
|
return oauthblueprints
|
||||||
|
|
||||||
|
|
||||||
|
if ub.oauth_support:
|
||||||
|
oauthblueprints = generate_oauth_blueprints()
|
||||||
|
|
||||||
|
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
|
||||||
|
def github_logged_in(blueprint, token):
|
||||||
|
if not token:
|
||||||
|
flash(_(u"Failed to log in with GitHub."), category="error")
|
||||||
|
log.error("Failed to log in with GitHub")
|
||||||
|
return False
|
||||||
|
|
||||||
|
resp = blueprint.session.get("/user")
|
||||||
|
if not resp.ok:
|
||||||
|
flash(_(u"Failed to fetch user info from GitHub."), category="error")
|
||||||
|
log.error("Failed to fetch user info from GitHub")
|
||||||
|
return False
|
||||||
|
|
||||||
|
github_info = resp.json()
|
||||||
|
github_user_id = str(github_info["id"])
|
||||||
|
return oauth_update_token(str(oauthblueprints[0]['id']), token, github_user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
|
||||||
|
def google_logged_in(blueprint, token):
|
||||||
|
if not token:
|
||||||
|
flash(_(u"Failed to log in with Google."), category="error")
|
||||||
|
log.error("Failed to log in with Google")
|
||||||
|
return False
|
||||||
|
|
||||||
|
resp = blueprint.session.get("/oauth2/v2/userinfo")
|
||||||
|
if not resp.ok:
|
||||||
|
flash(_(u"Failed to fetch user info from Google."), category="error")
|
||||||
|
log.error("Failed to fetch user info from Google")
|
||||||
|
return False
|
||||||
|
|
||||||
|
google_info = resp.json()
|
||||||
|
google_user_id = str(google_info["id"])
|
||||||
|
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
|
||||||
|
|
||||||
|
|
||||||
# notify on OAuth provider error
|
# notify on OAuth provider error
|
||||||
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
|
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
|
||||||
@@ -293,17 +305,35 @@ if ub.oauth_support:
|
|||||||
) # ToDo: Translate
|
) # ToDo: Translate
|
||||||
flash(msg, category="error")
|
flash(msg, category="error")
|
||||||
|
|
||||||
|
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
|
||||||
|
def google_error(blueprint, error, error_description=None, error_uri=None):
|
||||||
|
msg = (
|
||||||
|
u"OAuth error from {name}! "
|
||||||
|
u"error={error} description={description} uri={uri}"
|
||||||
|
).format(
|
||||||
|
name=blueprint.name,
|
||||||
|
error=error,
|
||||||
|
description=error_description,
|
||||||
|
uri=error_uri,
|
||||||
|
) # ToDo: Translate
|
||||||
|
flash(msg, category="error")
|
||||||
|
|
||||||
|
|
||||||
@oauth.route('/link/github')
|
@oauth.route('/link/github')
|
||||||
@oauth_required
|
@oauth_required
|
||||||
def github_login():
|
def github_login():
|
||||||
if not github.authorized:
|
if not github.authorized:
|
||||||
return redirect(url_for('github.login'))
|
return redirect(url_for('github.login'))
|
||||||
|
try:
|
||||||
account_info = github.get('/user')
|
account_info = github.get('/user')
|
||||||
if account_info.ok:
|
if account_info.ok:
|
||||||
account_info_json = account_info.json()
|
account_info_json = account_info.json()
|
||||||
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
|
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
|
||||||
flash(_(u"GitHub Oauth error, please retry later."), category="error")
|
flash(_(u"GitHub Oauth error, please retry later."), category="error")
|
||||||
|
log.error("GitHub Oauth error, please retry later")
|
||||||
|
except (InvalidGrantError, TokenExpiredError) as e:
|
||||||
|
flash(_(u"GitHub Oauth error: {}").format(e), category="error")
|
||||||
|
log.error(e)
|
||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
|
|
||||||
@@ -318,28 +348,19 @@ if ub.oauth_support:
|
|||||||
def google_login():
|
def google_login():
|
||||||
if not google.authorized:
|
if not google.authorized:
|
||||||
return redirect(url_for("google.login"))
|
return redirect(url_for("google.login"))
|
||||||
|
try:
|
||||||
resp = google.get("/oauth2/v2/userinfo")
|
resp = google.get("/oauth2/v2/userinfo")
|
||||||
if resp.ok:
|
if resp.ok:
|
||||||
account_info_json = resp.json()
|
account_info_json = resp.json()
|
||||||
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
|
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
|
||||||
flash(_(u"Google Oauth error, please retry later."), category="error")
|
flash(_(u"Google Oauth error, please retry later."), category="error")
|
||||||
|
log.error("Google Oauth error, please retry later")
|
||||||
|
except (InvalidGrantError, TokenExpiredError) as e:
|
||||||
|
flash(_(u"Google Oauth error: {}").format(e), category="error")
|
||||||
|
log.error(e)
|
||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
|
|
||||||
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
|
|
||||||
def google_error(blueprint, error, error_description=None, error_uri=None):
|
|
||||||
msg = (
|
|
||||||
u"OAuth error from {name}! "
|
|
||||||
u"error={error} description={description} uri={uri}"
|
|
||||||
).format(
|
|
||||||
name=blueprint.name,
|
|
||||||
error=error,
|
|
||||||
description=error_description,
|
|
||||||
uri=error_uri,
|
|
||||||
) # ToDo: Translate
|
|
||||||
flash(msg, category="error")
|
|
||||||
|
|
||||||
|
|
||||||
@oauth.route('/unlink/google', methods=["GET"])
|
@oauth.route('/unlink/google', methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def google_login_unlink():
|
def google_login_unlink():
|
||||||
|
|||||||
+148
-31
@@ -20,14 +20,13 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import datetime
|
import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import Blueprint, request, render_template, Response, g, make_response, abort
|
from flask import Blueprint, request, render_template, Response, g, make_response, abort
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from sqlalchemy.sql.expression import func, text, or_, and_
|
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
|
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
|
||||||
@@ -94,7 +93,45 @@ def feed_cc_search(query):
|
|||||||
@opds.route("/opds/search", methods=["GET"])
|
@opds.route("/opds/search", methods=["GET"])
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_normal_search():
|
def feed_normal_search():
|
||||||
return feed_search(request.args.get("query").strip())
|
return feed_search(request.args.get("query", "").strip())
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/books")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_booksindex():
|
||||||
|
shift = 0
|
||||||
|
off = int(request.args.get("offset") or 0)
|
||||||
|
entries = calibre_db.session.query(func.upper(func.substr(db.Books.sort, 1, 1)).label('id'))\
|
||||||
|
.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(db.Books.sort, 1, 1))).all()
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
if off == 0:
|
||||||
|
elements.append({'id': "00", 'name':_("All")})
|
||||||
|
shift = 1
|
||||||
|
for entry in entries[
|
||||||
|
off + shift - 1:
|
||||||
|
int(off + int(config.config_books_per_page) - shift)]:
|
||||||
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
|
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
len(entries) + 1)
|
||||||
|
return render_xml_template('feed.xml',
|
||||||
|
letterelements=elements,
|
||||||
|
folder='opds.feed_letter_books',
|
||||||
|
pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/books/letter/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_letter_books(book_id):
|
||||||
|
off = request.args.get("offset") or 0
|
||||||
|
letter = true() if book_id == "00" else func.upper(db.Books.sort).startswith(book_id)
|
||||||
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
|
||||||
|
db.Books,
|
||||||
|
letter,
|
||||||
|
[db.Books.sort])
|
||||||
|
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/new")
|
@opds.route("/opds/new")
|
||||||
@@ -150,14 +187,41 @@ def feed_hot():
|
|||||||
@opds.route("/opds/author")
|
@opds.route("/opds/author")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_authorindex():
|
def feed_authorindex():
|
||||||
off = request.args.get("offset") or 0
|
shift = 0
|
||||||
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
|
off = int(request.args.get("offset") or 0)
|
||||||
.filter(calibre_db.common_filters())\
|
entries = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('id'))\
|
||||||
.group_by(text('books_authors_link.author'))\
|
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters())\
|
||||||
.order_by(db.Authors.sort).limit(config.config_books_per_page)\
|
.group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all()
|
||||||
.offset(off)
|
|
||||||
|
elements = []
|
||||||
|
if off == 0:
|
||||||
|
elements.append({'id': "00", 'name':_("All")})
|
||||||
|
shift = 1
|
||||||
|
for entry in entries[
|
||||||
|
off + shift - 1:
|
||||||
|
int(off + int(config.config_books_per_page) - shift)]:
|
||||||
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
|
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(calibre_db.session.query(db.Authors).all()))
|
len(entries) + 1)
|
||||||
|
return render_xml_template('feed.xml',
|
||||||
|
letterelements=elements,
|
||||||
|
folder='opds.feed_letter_author',
|
||||||
|
pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/author/letter/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_letter_author(book_id):
|
||||||
|
off = request.args.get("offset") or 0
|
||||||
|
letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id)
|
||||||
|
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
|
||||||
|
.filter(calibre_db.common_filters()).filter(letter)\
|
||||||
|
.group_by(text('books_authors_link.author'))\
|
||||||
|
.order_by(db.Authors.sort)
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
entries.count())
|
||||||
|
entries = entries.limit(config.config_books_per_page).offset(off).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -201,17 +265,41 @@ def feed_publisher(book_id):
|
|||||||
@opds.route("/opds/category")
|
@opds.route("/opds/category")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_categoryindex():
|
def feed_categoryindex():
|
||||||
|
shift = 0
|
||||||
|
off = int(request.args.get("offset") or 0)
|
||||||
|
entries = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('id'))\
|
||||||
|
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters())\
|
||||||
|
.group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
|
||||||
|
elements = []
|
||||||
|
if off == 0:
|
||||||
|
elements.append({'id': "00", 'name':_("All")})
|
||||||
|
shift = 1
|
||||||
|
for entry in entries[
|
||||||
|
off + shift - 1:
|
||||||
|
int(off + int(config.config_books_per_page) - shift)]:
|
||||||
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
|
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
len(entries) + 1)
|
||||||
|
return render_xml_template('feed.xml',
|
||||||
|
letterelements=elements,
|
||||||
|
folder='opds.feed_letter_category',
|
||||||
|
pagination=pagination)
|
||||||
|
|
||||||
|
@opds.route("/opds/category/letter/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_letter_category(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
|
letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id)
|
||||||
entries = calibre_db.session.query(db.Tags)\
|
entries = calibre_db.session.query(db.Tags)\
|
||||||
.join(db.books_tags_link)\
|
.join(db.books_tags_link)\
|
||||||
.join(db.Books)\
|
.join(db.Books)\
|
||||||
.filter(calibre_db.common_filters())\
|
.filter(calibre_db.common_filters()).filter(letter)\
|
||||||
.group_by(text('books_tags_link.tag'))\
|
.group_by(text('books_tags_link.tag'))\
|
||||||
.order_by(db.Tags.name)\
|
.order_by(db.Tags.name)
|
||||||
.offset(off)\
|
|
||||||
.limit(config.config_books_per_page)
|
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(calibre_db.session.query(db.Tags).all()))
|
entries.count())
|
||||||
|
entries = entries.offset(off).limit(config.config_books_per_page).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -229,16 +317,40 @@ def feed_category(book_id):
|
|||||||
@opds.route("/opds/series")
|
@opds.route("/opds/series")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_seriesindex():
|
def feed_seriesindex():
|
||||||
|
shift = 0
|
||||||
|
off = int(request.args.get("offset") or 0)
|
||||||
|
entries = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('id'))\
|
||||||
|
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters())\
|
||||||
|
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
|
||||||
|
elements = []
|
||||||
|
if off == 0:
|
||||||
|
elements.append({'id': "00", 'name':_("All")})
|
||||||
|
shift = 1
|
||||||
|
for entry in entries[
|
||||||
|
off + shift - 1:
|
||||||
|
int(off + int(config.config_books_per_page) - shift)]:
|
||||||
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
len(entries) + 1)
|
||||||
|
return render_xml_template('feed.xml',
|
||||||
|
letterelements=elements,
|
||||||
|
folder='opds.feed_letter_series',
|
||||||
|
pagination=pagination)
|
||||||
|
|
||||||
|
@opds.route("/opds/series/letter/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_letter_series(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
|
letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id)
|
||||||
entries = calibre_db.session.query(db.Series)\
|
entries = calibre_db.session.query(db.Series)\
|
||||||
.join(db.books_series_link)\
|
.join(db.books_series_link)\
|
||||||
.join(db.Books)\
|
.join(db.Books)\
|
||||||
.filter(calibre_db.common_filters())\
|
.filter(calibre_db.common_filters()).filter(letter)\
|
||||||
.group_by(text('books_series_link.series'))\
|
.group_by(text('books_series_link.series'))\
|
||||||
.order_by(db.Series.sort)\
|
.order_by(db.Series.sort)
|
||||||
.offset(off).all()
|
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(calibre_db.session.query(db.Series).all()))
|
entries.count())
|
||||||
|
entries = entries.offset(off).limit(config.config_books_per_page).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -269,7 +381,7 @@ def feed_ratingindex():
|
|||||||
len(entries))
|
len(entries))
|
||||||
element = list()
|
element = list()
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
element.append(FeedObject(entry[0].id, "{} Stars".format(entry.name)))
|
element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name)))
|
||||||
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -320,16 +432,17 @@ def feed_languagesindex():
|
|||||||
if current_user.filter_language() == u"all":
|
if current_user.filter_language() == u"all":
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
else:
|
else:
|
||||||
try:
|
#try:
|
||||||
cur_l = LC.parse(current_user.filter_language())
|
# cur_l = LC.parse(current_user.filter_language())
|
||||||
except UnknownLocaleError:
|
#except UnknownLocaleError:
|
||||||
cur_l = None
|
# cur_l = None
|
||||||
languages = calibre_db.session.query(db.Languages).filter(
|
languages = calibre_db.session.query(db.Languages).filter(
|
||||||
db.Languages.lang_code == current_user.filter_language()).all()
|
db.Languages.lang_code == current_user.filter_language()).all()
|
||||||
if cur_l:
|
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
|
||||||
languages[0].name = cur_l.get_language_name(get_locale())
|
#if cur_l:
|
||||||
else:
|
# languages[0].name = cur_l.get_language_name(get_locale())
|
||||||
languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
|
#else:
|
||||||
|
# languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(languages))
|
len(languages))
|
||||||
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination)
|
||||||
@@ -423,14 +536,18 @@ def feed_search(term):
|
|||||||
|
|
||||||
|
|
||||||
def check_auth(username, password):
|
def check_auth(username, password):
|
||||||
if sys.version_info.major == 3:
|
|
||||||
try:
|
try:
|
||||||
username = username.encode('windows-1252')
|
username = username.encode('windows-1252')
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
username = username.encode('utf-8')
|
username = username.encode('utf-8')
|
||||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) ==
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
|
||||||
username.decode('utf-8').lower()).first()
|
username.decode('utf-8').lower()).first()
|
||||||
return bool(user and check_password_hash(str(user.password), password))
|
if bool(user and check_password_hash(str(user.password), password)):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||||
|
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_Address)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def authenticate():
|
def authenticate():
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
# http://flask.pocoo.org/snippets/62/
|
# http://flask.pocoo.org/snippets/62/
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
+3
-3
@@ -62,7 +62,7 @@ def remote_login():
|
|||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
|
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
|
||||||
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
|
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
|
||||||
return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
|
return render_title_template('remote_login.html', title=_(u"Login"), token=auth_token.auth_token,
|
||||||
verify_url=verify_url, page="remotelogin")
|
verify_url=verify_url, page="remotelogin")
|
||||||
|
|
||||||
|
|
||||||
@@ -126,11 +126,11 @@ def token_verified():
|
|||||||
login_user(user)
|
login_user(user)
|
||||||
|
|
||||||
ub.session.delete(auth_token)
|
ub.session.delete(auth_token)
|
||||||
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.nickname))
|
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
|
||||||
|
|
||||||
data['status'] = 'success'
|
data['status'] = 'success'
|
||||||
log.debug(u"Remote Login for userid %s succeded", user.id)
|
log.debug(u"Remote Login for userid %s succeded", user.id)
|
||||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
|
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
|
||||||
|
|
||||||
response = make_response(json.dumps(data, ensure_ascii=False))
|
response = make_response(json.dumps(data, ensure_ascii=False))
|
||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
|
|||||||
@@ -36,12 +36,18 @@ def get_sidebar_config(kwargs=None):
|
|||||||
else:
|
else:
|
||||||
content = 'conf' in kwargs
|
content = 'conf' in kwargs
|
||||||
sidebar = list()
|
sidebar = list()
|
||||||
sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new",
|
sidebar.append({"glyph": "glyphicon-book", "text": _('Books'), "link": 'web.index', "id": "new",
|
||||||
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
|
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
|
||||||
"show_text": _('Show recent books'), "config_show":False})
|
"show_text": _('Show recent books'), "config_show":False})
|
||||||
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
|
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
|
||||||
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
|
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
|
||||||
"show_text": _('Show Hot Books'), "config_show": True})
|
"show_text": _('Show Hot Books'), "config_show": True})
|
||||||
|
if current_user.role_admin():
|
||||||
|
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
|
||||||
|
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
||||||
|
"page": "download", "show_text": _('Show Downloaded Books'),
|
||||||
|
"config_show": content})
|
||||||
|
else:
|
||||||
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
||||||
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
||||||
"page": "download", "show_text": _('Show Downloaded Books'),
|
"page": "download", "show_text": _('Show Downloaded Books'),
|
||||||
@@ -59,7 +65,7 @@ def get_sidebar_config(kwargs=None):
|
|||||||
"show_text": _('Show unread'), "config_show": False})
|
"show_text": _('Show unread'), "config_show": False})
|
||||||
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
|
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
|
||||||
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
||||||
"show_text": _('Show random books'), "config_show": True})
|
"show_text": _('Show Random Books'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
||||||
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
||||||
"show_text": _('Show category selection'), "config_show": True})
|
"show_text": _('Show category selection'), "config_show": True})
|
||||||
|
|||||||
@@ -36,8 +36,6 @@
|
|||||||
#
|
#
|
||||||
# Inspired by http://flask.pocoo.org/snippets/35/
|
# Inspired by http://flask.pocoo.org/snippets/35/
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
class ReverseProxied(object):
|
class ReverseProxied(object):
|
||||||
"""Wrap the application in this middleware and configure the
|
"""Wrap the application in this middleware and configure the
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
import datetime
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
from flask import Blueprint, request, Response, url_for
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_login import login_required
|
||||||
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||||
|
|
||||||
|
from . import constants, logger, ub
|
||||||
|
from cps.services.Metadata import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
meta = Blueprint('metadata', __name__)
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
new_list = list()
|
||||||
|
meta_dir = os.path.join(constants.BASE_DIR, "cps", "metadata_provider")
|
||||||
|
modules = os.listdir(os.path.join(constants.BASE_DIR, "cps", "metadata_provider"))
|
||||||
|
for f in modules:
|
||||||
|
if os.path.isfile(os.path.join(meta_dir, f)) and not f.endswith('__init__.py'):
|
||||||
|
a = os.path.basename(f)[:-3]
|
||||||
|
try:
|
||||||
|
importlib.import_module("cps.metadata_provider." + a)
|
||||||
|
new_list.append(a)
|
||||||
|
except ImportError:
|
||||||
|
log.error("Import error for metadata source: {}".format(a))
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_classes(provider_list):
|
||||||
|
classes = list()
|
||||||
|
for element in provider_list:
|
||||||
|
for name, obj in inspect.getmembers(sys.modules["cps.metadata_provider." + element]):
|
||||||
|
if inspect.isclass(obj) and name != "Metadata" and issubclass(obj, Metadata):
|
||||||
|
classes.append(obj())
|
||||||
|
return classes
|
||||||
|
|
||||||
|
cl = list_classes(new_list)
|
||||||
|
|
||||||
|
@meta.route("/metadata/provider")
|
||||||
|
@login_required
|
||||||
|
def metadata_provider():
|
||||||
|
active = current_user.view_settings.get('metadata', {})
|
||||||
|
provider = list()
|
||||||
|
for c in cl:
|
||||||
|
ac = active.get(c.__id__, True)
|
||||||
|
provider.append({"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__})
|
||||||
|
return Response(json.dumps(provider), mimetype='application/json')
|
||||||
|
|
||||||
|
@meta.route("/metadata/provider", methods=['POST'])
|
||||||
|
@meta.route("/metadata/provider/<prov_name>", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def metadata_change_active_provider(prov_name):
|
||||||
|
new_state = request.get_json()
|
||||||
|
active = current_user.view_settings.get('metadata', {})
|
||||||
|
active[new_state['id']] = new_state['value']
|
||||||
|
current_user.view_settings['metadata'] = active
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
flag_modified(current_user, "view_settings")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
ub.session.commit()
|
||||||
|
except (InvalidRequestError, OperationalError):
|
||||||
|
log.error("Invalid request received: {}".format(request))
|
||||||
|
return "Invalid request", 400
|
||||||
|
if "initial" in new_state and prov_name:
|
||||||
|
for c in cl:
|
||||||
|
if c.__id__ == prov_name:
|
||||||
|
data = c.search(new_state.get('query', ""))
|
||||||
|
break
|
||||||
|
return Response(json.dumps(data), mimetype='application/json')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@meta.route("/metadata/search", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def metadata_search():
|
||||||
|
query = request.form.to_dict().get('query')
|
||||||
|
data = list()
|
||||||
|
active = current_user.view_settings.get('metadata', {})
|
||||||
|
if query:
|
||||||
|
static_cover = url_for('static', filename='generic_cover.jpg')
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
|
meta = {executor.submit(c.search, query, static_cover): c for c in cl if active.get(c.__id__, True)}
|
||||||
|
for future in concurrent.futures.as_completed(meta):
|
||||||
|
data.extend(future.result())
|
||||||
|
return Response(json.dumps(data), mimetype='application/json')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+64
-8
@@ -16,12 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import errno
|
import errno
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess # nosec
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from gevent.pywsgi import WSGIServer
|
from gevent.pywsgi import WSGIServer
|
||||||
@@ -136,6 +136,64 @@ class WebServer(object):
|
|||||||
|
|
||||||
return sock, _readable_listen_address(*address)
|
return sock, _readable_listen_address(*address)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_args_for_reloading():
|
||||||
|
"""Determine how the script was executed, and return the args needed
|
||||||
|
to execute it again in a new process.
|
||||||
|
Code from https://github.com/pyload/pyload. Author GammaC0de, voulter
|
||||||
|
"""
|
||||||
|
rv = [sys.executable]
|
||||||
|
py_script = sys.argv[0]
|
||||||
|
args = sys.argv[1:]
|
||||||
|
# Need to look at main module to determine how it was executed.
|
||||||
|
__main__ = sys.modules["__main__"]
|
||||||
|
|
||||||
|
# The value of __package__ indicates how Python was called. It may
|
||||||
|
# not exist if a setuptools script is installed as an egg. It may be
|
||||||
|
# set incorrectly for entry points created with pip on Windows.
|
||||||
|
if getattr(__main__, "__package__", None) is None or (
|
||||||
|
os.name == "nt"
|
||||||
|
and __main__.__package__ == ""
|
||||||
|
and not os.path.exists(py_script)
|
||||||
|
and os.path.exists("{}.exe".format(py_script))
|
||||||
|
):
|
||||||
|
# Executed a file, like "python app.py".
|
||||||
|
py_script = os.path.abspath(py_script)
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
# Windows entry points have ".exe" extension and should be
|
||||||
|
# called directly.
|
||||||
|
if not os.path.exists(py_script) and os.path.exists("{}.exe".format(py_script)):
|
||||||
|
py_script += ".exe"
|
||||||
|
|
||||||
|
if (
|
||||||
|
os.path.splitext(sys.executable)[1] == ".exe"
|
||||||
|
and os.path.splitext(py_script)[1] == ".exe"
|
||||||
|
):
|
||||||
|
rv.pop(0)
|
||||||
|
|
||||||
|
rv.append(py_script)
|
||||||
|
else:
|
||||||
|
# Executed a module, like "python -m module".
|
||||||
|
if sys.argv[0] == "-m":
|
||||||
|
args = sys.argv
|
||||||
|
else:
|
||||||
|
if os.path.isfile(py_script):
|
||||||
|
# Rewritten by Python from "-m script" to "/path/to/script.py".
|
||||||
|
py_module = __main__.__package__
|
||||||
|
name = os.path.splitext(os.path.basename(py_script))[0]
|
||||||
|
|
||||||
|
if name != "__main__":
|
||||||
|
py_module += ".{}".format(name)
|
||||||
|
else:
|
||||||
|
# Incorrectly rewritten by pydevd debugger from "-m script" to "script".
|
||||||
|
py_module = py_script
|
||||||
|
|
||||||
|
rv.extend(("-m", py_module.lstrip(".")))
|
||||||
|
|
||||||
|
rv.extend(args)
|
||||||
|
return rv
|
||||||
|
|
||||||
def _start_gevent(self):
|
def _start_gevent(self):
|
||||||
ssl_args = self.ssl_args or {}
|
ssl_args = self.ssl_args or {}
|
||||||
|
|
||||||
@@ -192,18 +250,16 @@ class WebServer(object):
|
|||||||
finally:
|
finally:
|
||||||
self.wsgiserver = None
|
self.wsgiserver = None
|
||||||
|
|
||||||
|
# prevent irritating log of pending tasks message from asyncio
|
||||||
|
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
|
||||||
|
|
||||||
if not self.restart:
|
if not self.restart:
|
||||||
log.info("Performing shutdown of Calibre-Web")
|
log.info("Performing shutdown of Calibre-Web")
|
||||||
# prevent irritiating log of pending tasks message from asyncio
|
|
||||||
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
log.info("Performing restart of Calibre-Web")
|
log.info("Performing restart of Calibre-Web")
|
||||||
arguments = list(sys.argv)
|
args = self._get_args_for_reloading()
|
||||||
arguments.insert(0, sys.executable)
|
subprocess.call(args, close_fds=True) # nosec
|
||||||
if os.name == 'nt':
|
|
||||||
arguments = ["\"%s\"" % a for a in arguments]
|
|
||||||
os.execv(sys.executable, arguments)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _killServer(self, __, ___):
|
def _killServer(self, __, ___):
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
class Metadata():
|
||||||
|
__name__ = "Generic"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.active = True
|
||||||
|
|
||||||
|
def set_status(self, state):
|
||||||
|
self.active = state
|
||||||
+23
-16
@@ -22,6 +22,7 @@ from base64 import b64decode, b64encode
|
|||||||
from jsonschema import validate, exceptions, __version__
|
from jsonschema import validate, exceptions, __version__
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
try:
|
try:
|
||||||
|
# pylint: disable=unused-import
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
@@ -34,9 +35,6 @@ log = logger.create()
|
|||||||
|
|
||||||
|
|
||||||
def b64encode_json(json_data):
|
def b64encode_json(json_data):
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
return b64encode(json.dumps(json_data))
|
|
||||||
else:
|
|
||||||
return b64encode(json.dumps(json_data).encode())
|
return b64encode(json.dumps(json_data).encode())
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +62,7 @@ class SyncToken:
|
|||||||
books_last_modified: Datetime representing the last modified book that the device knows about.
|
books_last_modified: Datetime representing the last modified book that the device knows about.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
|
SYNC_TOKEN_HEADER = "x-kobo-synctoken" # nosec
|
||||||
VERSION = "1-1-0"
|
VERSION = "1-1-0"
|
||||||
LAST_MODIFIED_ADDED_VERSION = "1-1-0"
|
LAST_MODIFIED_ADDED_VERSION = "1-1-0"
|
||||||
MIN_VERSION = "1-0-0"
|
MIN_VERSION = "1-0-0"
|
||||||
@@ -84,8 +82,8 @@ class SyncToken:
|
|||||||
"books_last_created": {"type": "string"},
|
"books_last_created": {"type": "string"},
|
||||||
"archive_last_modified": {"type": "string"},
|
"archive_last_modified": {"type": "string"},
|
||||||
"reading_state_last_modified": {"type": "string"},
|
"reading_state_last_modified": {"type": "string"},
|
||||||
"tags_last_modified": {"type": "string"},
|
"tags_last_modified": {"type": "string"}
|
||||||
"books_last_id": {"type": "integer", "optional": True}
|
# "books_last_id": {"type": "integer", "optional": True}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,21 +94,21 @@ class SyncToken:
|
|||||||
books_last_modified=datetime.min,
|
books_last_modified=datetime.min,
|
||||||
archive_last_modified=datetime.min,
|
archive_last_modified=datetime.min,
|
||||||
reading_state_last_modified=datetime.min,
|
reading_state_last_modified=datetime.min,
|
||||||
tags_last_modified=datetime.min,
|
tags_last_modified=datetime.min
|
||||||
books_last_id=-1
|
# books_last_id=-1
|
||||||
):
|
): # nosec
|
||||||
self.raw_kobo_store_token = raw_kobo_store_token
|
self.raw_kobo_store_token = raw_kobo_store_token
|
||||||
self.books_last_created = books_last_created
|
self.books_last_created = books_last_created
|
||||||
self.books_last_modified = books_last_modified
|
self.books_last_modified = books_last_modified
|
||||||
self.archive_last_modified = archive_last_modified
|
self.archive_last_modified = archive_last_modified
|
||||||
self.reading_state_last_modified = reading_state_last_modified
|
self.reading_state_last_modified = reading_state_last_modified
|
||||||
self.tags_last_modified = tags_last_modified
|
self.tags_last_modified = tags_last_modified
|
||||||
self.books_last_id = books_last_id
|
# self.books_last_id = books_last_id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_headers(headers):
|
def from_headers(headers):
|
||||||
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
|
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
|
||||||
if sync_token_header == "":
|
if sync_token_header == "": # nosec
|
||||||
return SyncToken()
|
return SyncToken()
|
||||||
|
|
||||||
# On the first sync from a Kobo device, we may receive the SyncToken
|
# On the first sync from a Kobo device, we may receive the SyncToken
|
||||||
@@ -140,12 +138,12 @@ class SyncToken:
|
|||||||
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
||||||
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
||||||
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
||||||
books_last_id = data_json["books_last_id"]
|
# books_last_id = data_json["books_last_id"]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||||
except KeyError:
|
#except KeyError:
|
||||||
books_last_id = -1
|
# books_last_id = -1
|
||||||
|
|
||||||
return SyncToken(
|
return SyncToken(
|
||||||
raw_kobo_store_token=raw_kobo_store_token,
|
raw_kobo_store_token=raw_kobo_store_token,
|
||||||
@@ -154,7 +152,7 @@ class SyncToken:
|
|||||||
archive_last_modified=archive_last_modified,
|
archive_last_modified=archive_last_modified,
|
||||||
reading_state_last_modified=reading_state_last_modified,
|
reading_state_last_modified=reading_state_last_modified,
|
||||||
tags_last_modified=tags_last_modified,
|
tags_last_modified=tags_last_modified,
|
||||||
books_last_id=books_last_id
|
#books_last_id=books_last_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_kobo_store_header(self, store_headers):
|
def set_kobo_store_header(self, store_headers):
|
||||||
@@ -178,7 +176,16 @@ class SyncToken:
|
|||||||
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
||||||
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
||||||
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
|
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
|
||||||
"books_last_id":self.books_last_id
|
#"books_last_id":self.books_last_id
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return b64encode_json(token)
|
return b64encode_json(token)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{},{},{},{},{},{}".format(self.raw_kobo_store_token,
|
||||||
|
self.books_last_created,
|
||||||
|
self.books_last_modified,
|
||||||
|
self.archive_last_modified,
|
||||||
|
self.reading_state_last_modified,
|
||||||
|
self.tags_last_modified)
|
||||||
|
#self.books_last_id)
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
|
|
||||||
from .. import logger
|
from .. import logger
|
||||||
|
|
||||||
|
|
||||||
@@ -45,3 +43,9 @@ except ImportError as err:
|
|||||||
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
|
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
|
||||||
kobo = None
|
kobo = None
|
||||||
SyncToken = None
|
SyncToken = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import gmail
|
||||||
|
except ImportError as err:
|
||||||
|
log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err)
|
||||||
|
gmail = None
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
from google.auth.transport.requests import Request
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import base64
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
from ..constants import BASE_DIR
|
||||||
|
from .. import logger
|
||||||
|
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
SCOPES = ['openid', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/userinfo.email']
|
||||||
|
|
||||||
|
def setup_gmail(token):
|
||||||
|
# If there are no (valid) credentials available, let the user log in.
|
||||||
|
creds = None
|
||||||
|
if "token" in token:
|
||||||
|
creds = Credentials(
|
||||||
|
token=token['token'],
|
||||||
|
refresh_token=token['refresh_token'],
|
||||||
|
token_uri=token['token_uri'],
|
||||||
|
client_id=token['client_id'],
|
||||||
|
client_secret=token['client_secret'],
|
||||||
|
scopes=token['scopes'],
|
||||||
|
)
|
||||||
|
creds.expiry = datetime.fromisoformat(token['expiry'])
|
||||||
|
|
||||||
|
if not creds or not creds.valid:
|
||||||
|
# don't forget to dump one more time after the refresh
|
||||||
|
# also, some file-locking routines wouldn't be needless
|
||||||
|
if creds and creds.expired and creds.refresh_token:
|
||||||
|
creds.refresh(Request())
|
||||||
|
else:
|
||||||
|
cred_file = os.path.join(BASE_DIR, 'gmail.json')
|
||||||
|
if not os.path.exists(cred_file):
|
||||||
|
raise Exception(_("Found no valid gmail.json file with OAuth information"))
|
||||||
|
flow = InstalledAppFlow.from_client_secrets_file(
|
||||||
|
os.path.join(BASE_DIR, 'gmail.json'), SCOPES)
|
||||||
|
creds = flow.run_local_server(port=0)
|
||||||
|
user_info = get_user_info(creds)
|
||||||
|
return {
|
||||||
|
'token': creds.token,
|
||||||
|
'refresh_token': creds.refresh_token,
|
||||||
|
'token_uri': creds.token_uri,
|
||||||
|
'client_id': creds.client_id,
|
||||||
|
'client_secret': creds.client_secret,
|
||||||
|
'scopes': creds.scopes,
|
||||||
|
'expiry': creds.expiry.isoformat(),
|
||||||
|
'email': user_info
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_user_info(credentials):
|
||||||
|
user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials)
|
||||||
|
user_info = user_info_service.userinfo().get().execute()
|
||||||
|
return user_info.get('email', "")
|
||||||
|
|
||||||
|
def send_messsage(token, msg):
|
||||||
|
log.debug("Start sending e-mail via Gmail")
|
||||||
|
creds = Credentials(
|
||||||
|
token=token['token'],
|
||||||
|
refresh_token=token['refresh_token'],
|
||||||
|
token_uri=token['token_uri'],
|
||||||
|
client_id=token['client_id'],
|
||||||
|
client_secret=token['client_secret'],
|
||||||
|
scopes=token['scopes'],
|
||||||
|
)
|
||||||
|
creds.expiry = datetime.fromisoformat(token['expiry'])
|
||||||
|
if creds and creds.expired and creds.refresh_token:
|
||||||
|
creds.refresh(Request())
|
||||||
|
service = build('gmail', 'v1', credentials=creds)
|
||||||
|
message_as_bytes = msg.as_bytes() # the message should converted from string to bytes.
|
||||||
|
message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) # encode in base64 (printable letters coding)
|
||||||
|
raw = message_as_base64.decode() # convert to something JSON serializable
|
||||||
|
body = {'raw': raw}
|
||||||
|
|
||||||
|
(service.users().messages().send(userId='me', body=body).execute())
|
||||||
|
log.debug("E-mail send successfully via Gmail")
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import time
|
import time
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from flask_simpleldap import LDAP, LDAPException
|
from flask_simpleldap import LDAP, LDAPException
|
||||||
|
|||||||
+27
-7
@@ -1,5 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2020 pwr
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import threading
|
import threading
|
||||||
import abc
|
import abc
|
||||||
import uuid
|
import uuid
|
||||||
@@ -69,6 +85,7 @@ class WorkerThread(threading.Thread):
|
|||||||
def add(cls, user, task):
|
def add(cls, user, task):
|
||||||
ins = cls.getInstance()
|
ins = cls.getInstance()
|
||||||
ins.num += 1
|
ins.num += 1
|
||||||
|
log.debug("Add Task for user: {}: {}".format(user, task))
|
||||||
ins.queue.put(QueuedTask(
|
ins.queue.put(QueuedTask(
|
||||||
num=ins.num,
|
num=ins.num,
|
||||||
user=user,
|
user=user,
|
||||||
@@ -159,9 +176,9 @@ class CalibreTask:
|
|||||||
# catch any unhandled exceptions in a task and automatically fail it
|
# catch any unhandled exceptions in a task and automatically fail it
|
||||||
try:
|
try:
|
||||||
self.run(*args)
|
self.run(*args)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
self._handleError(str(e))
|
self._handleError(str(ex))
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
|
|
||||||
self.end_time = datetime.now()
|
self.end_time = datetime.now()
|
||||||
|
|
||||||
@@ -204,10 +221,13 @@ class CalibreTask:
|
|||||||
# By default, we're good to clean a task if it's "Done"
|
# By default, we're good to clean a task if it's "Done"
|
||||||
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
||||||
|
|
||||||
@progress.setter
|
'''@progress.setter
|
||||||
def progress(self, x):
|
def progress(self, x):
|
||||||
# todo: throw error if outside of [0,1]
|
if x > 1:
|
||||||
self._progress = x
|
x = 1
|
||||||
|
if x < 0:
|
||||||
|
x = 0
|
||||||
|
self._progress = x'''
|
||||||
|
|
||||||
def _handleError(self, error_message):
|
def _handleError(self, error_message):
|
||||||
self.stat = STAT_FAIL
|
self.stat = STAT_FAIL
|
||||||
|
|||||||
+67
-44
@@ -20,21 +20,19 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
from datetime import datetime
|
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import Blueprint, request, flash, redirect, url_for
|
from flask import Blueprint, flash, redirect, request, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import login_required, current_user
|
from flask_login import current_user, login_required
|
||||||
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
from sqlalchemy.sql.expression import func, true
|
from sqlalchemy.sql.expression import func, true
|
||||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
|
||||||
|
|
||||||
from . import logger, ub, calibre_db, db
|
from . import calibre_db, config, db, logger, ub
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .usermanagement import login_required_if_no_ano
|
from .usermanagement import login_required_if_no_ano
|
||||||
|
|
||||||
|
|
||||||
shelf = Blueprint('shelf', __name__)
|
shelf = Blueprint('shelf', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -72,10 +70,9 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
|
|
||||||
if not check_shelf_edit_permissions(shelf):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
if not xhr:
|
if not xhr:
|
||||||
flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name),
|
flash(_(u"Sorry you are not allowed to add a book to that shelf"), category="error")
|
||||||
category="error")
|
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403
|
return "Sorry you are not allowed to add a book to the that shelf", 403
|
||||||
|
|
||||||
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
||||||
ub.BookShelf.book_id == book_id).first()
|
ub.BookShelf.book_id == book_id).first()
|
||||||
@@ -99,12 +96,14 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
|
log.error("Settings DB is not Writeable")
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
if not xhr:
|
if not xhr:
|
||||||
|
log.debug("Book has been added to shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
@@ -123,6 +122,7 @@ def search_to_shelf(shelf_id):
|
|||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
if not check_shelf_edit_permissions(shelf):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
|
log.warning("You are not allowed to add a book to the the shelf: {}".format(shelf.name))
|
||||||
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
|
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ def search_to_shelf(shelf_id):
|
|||||||
books_for_shelf = ub.searched_ids[current_user.id]
|
books_for_shelf = ub.searched_ids[current_user.id]
|
||||||
|
|
||||||
if not books_for_shelf:
|
if not books_for_shelf:
|
||||||
log.error("Books are already part of %s", shelf.name)
|
log.error("Books are already part of {}".format(shelf.name))
|
||||||
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
@@ -156,8 +156,10 @@ def search_to_shelf(shelf_id):
|
|||||||
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
else:
|
else:
|
||||||
|
log.error("Could not add books to shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
|
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
@@ -168,7 +170,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf is None:
|
if shelf is None:
|
||||||
log.error("Invalid shelf specified: %s", shelf_id)
|
log.error("Invalid shelf specified: {}".format(shelf_id))
|
||||||
if not xhr:
|
if not xhr:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Invalid shelf specified", 400
|
return "Invalid shelf specified", 400
|
||||||
@@ -197,7 +199,8 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
else:
|
else:
|
||||||
@@ -211,6 +214,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
return "", 204
|
return "", 204
|
||||||
else:
|
else:
|
||||||
if not xhr:
|
if not xhr:
|
||||||
|
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
|
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
|
||||||
category="error")
|
category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
@@ -221,74 +225,86 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def create_shelf():
|
def create_shelf():
|
||||||
shelf = ub.Shelf()
|
shelf = ub.Shelf()
|
||||||
return create_edit_shelf(shelf, title=_(u"Create a Shelf"), page="shelfcreate")
|
return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate")
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def edit_shelf(shelf_id):
|
def edit_shelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
return create_edit_shelf(shelf, title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
|
if not check_shelf_edit_permissions(shelf):
|
||||||
|
flash(_(u"Sorry you are not allowed to edit this shelf"), category="error")
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
return create_edit_shelf(shelf, page_title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
|
||||||
|
|
||||||
|
|
||||||
# if shelf ID is set, we are editing a shelf
|
# if shelf ID is set, we are editing a shelf
|
||||||
def create_edit_shelf(shelf, title, page, shelf_id=False):
|
def create_edit_shelf(shelf, page_title, page, shelf_id=False):
|
||||||
|
sync_only_selected_shelves = current_user.kobo_only_shelves_sync
|
||||||
|
# calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
if "is_public" in to_save:
|
shelf.is_public = 1 if to_save.get("is_public") else 0
|
||||||
shelf.is_public = 1
|
if config.config_kobo_sync:
|
||||||
else:
|
shelf.kobo_sync = True if to_save.get("kobo_sync") else False
|
||||||
shelf.is_public = 0
|
shelf_title = to_save.get("title", "")
|
||||||
if check_shelf_is_unique(shelf, to_save, shelf_id):
|
if check_shelf_is_unique(shelf, shelf_title, shelf_id):
|
||||||
shelf.name = to_save["title"]
|
shelf.name = shelf_title
|
||||||
# shelf.last_modified = datetime.utcnow()
|
|
||||||
if not shelf_id:
|
if not shelf_id:
|
||||||
shelf.user_id = int(current_user.id)
|
shelf.user_id = int(current_user.id)
|
||||||
ub.session.add(shelf)
|
ub.session.add(shelf)
|
||||||
shelf_action = "created"
|
shelf_action = "created"
|
||||||
flash_text = _(u"Shelf %(title)s created", title=to_save["title"])
|
flash_text = _(u"Shelf %(title)s created", title=shelf_title)
|
||||||
else:
|
else:
|
||||||
shelf_action = "changed"
|
shelf_action = "changed"
|
||||||
flash_text = _(u"Shelf %(title)s changed", title=to_save["title"])
|
flash_text = _(u"Shelf %(title)s changed", title=shelf_title)
|
||||||
try:
|
try:
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
log.info(u"Shelf {} {}".format(to_save["title"], shelf_action))
|
log.info(u"Shelf {} {}".format(shelf_title, shelf_action))
|
||||||
flash(flash_text, category="success")
|
flash(flash_text, category="success")
|
||||||
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as ex:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
except Exception as e:
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
except Exception as ex:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
flash(_(u"There was an error"), category="error")
|
flash(_(u"There was an error"), category="error")
|
||||||
return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page)
|
return render_title_template('shelf_edit.html',
|
||||||
|
shelf=shelf,
|
||||||
|
title=page_title,
|
||||||
|
page=page,
|
||||||
|
kobo_sync_enabled=config.config_kobo_sync,
|
||||||
|
sync_only_selected_shelves=sync_only_selected_shelves)
|
||||||
|
|
||||||
|
|
||||||
def check_shelf_is_unique(shelf, to_save, shelf_id=False):
|
def check_shelf_is_unique(shelf, title, shelf_id=False):
|
||||||
if shelf_id:
|
if shelf_id:
|
||||||
ident = ub.Shelf.id != shelf_id
|
ident = ub.Shelf.id != shelf_id
|
||||||
else:
|
else:
|
||||||
ident = true()
|
ident = true()
|
||||||
if shelf.is_public == 1:
|
if shelf.is_public == 1:
|
||||||
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||||
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1)) \
|
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \
|
||||||
.filter(ident) \
|
.filter(ident) \
|
||||||
.first() is None
|
.first() is None
|
||||||
|
|
||||||
if not is_shelf_name_unique:
|
if not is_shelf_name_unique:
|
||||||
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
log.error("A public shelf with the name '{}' already exists.".format(title))
|
||||||
|
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=title),
|
||||||
category="error")
|
category="error")
|
||||||
else:
|
else:
|
||||||
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||||
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) &
|
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 0) &
|
||||||
(ub.Shelf.user_id == int(current_user.id))) \
|
(ub.Shelf.user_id == int(current_user.id))) \
|
||||||
.filter(ident) \
|
.filter(ident) \
|
||||||
.first() is None
|
.first() is None
|
||||||
|
|
||||||
if not is_shelf_name_unique:
|
if not is_shelf_name_unique:
|
||||||
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
log.error("A private shelf with the name '{}' already exists.".format(title))
|
||||||
|
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=title),
|
||||||
category="error")
|
category="error")
|
||||||
return is_shelf_name_unique
|
return is_shelf_name_unique
|
||||||
|
|
||||||
@@ -311,7 +327,8 @@ def delete_shelf(shelf_id):
|
|||||||
delete_shelf_helper(cur_shelf)
|
delete_shelf_helper(cur_shelf)
|
||||||
except InvalidRequestError:
|
except InvalidRequestError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
@@ -345,7 +362,8 @@ def order_shelf(shelf_id):
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
result = list()
|
result = list()
|
||||||
@@ -360,7 +378,9 @@ def order_shelf(shelf_id):
|
|||||||
|
|
||||||
|
|
||||||
def change_shelf_order(shelf_id, order):
|
def change_shelf_order(shelf_id, order):
|
||||||
result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\
|
result = calibre_db.session.query(db.Books).outerjoin(db.books_series_link,
|
||||||
|
db.Books.id == db.books_series_link.c.book)\
|
||||||
|
.outerjoin(db.Series).join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id) \
|
||||||
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
|
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
|
||||||
for index, entry in enumerate(result):
|
for index, entry in enumerate(result):
|
||||||
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||||
@@ -390,9 +410,11 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
|||||||
if sort_param == 'old':
|
if sort_param == 'old':
|
||||||
change_shelf_order(shelf_id, [db.Books.timestamp])
|
change_shelf_order(shelf_id, [db.Books.timestamp])
|
||||||
if sort_param == 'authaz':
|
if sort_param == 'authaz':
|
||||||
change_shelf_order(shelf_id, [db.Books.author_sort.asc()])
|
change_shelf_order(shelf_id, [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index])
|
||||||
if sort_param == 'authza':
|
if sort_param == 'authza':
|
||||||
change_shelf_order(shelf_id, [db.Books.author_sort.desc()])
|
change_shelf_order(shelf_id, [db.Books.author_sort.desc(),
|
||||||
|
db.Series.name.desc(),
|
||||||
|
db.Books.series_index.desc()])
|
||||||
page = "shelf.html"
|
page = "shelf.html"
|
||||||
pagesize = 0
|
pagesize = 0
|
||||||
else:
|
else:
|
||||||
@@ -415,7 +437,8 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
|
||||||
return render_title_template(page,
|
return render_title_template(page,
|
||||||
entries=result,
|
entries=result,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user