Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9c0bcb4a2 | ||
|
|
e61e94f0fa | ||
|
|
85aac02593 | ||
|
|
9a896ea81e | ||
|
|
422c1893c0 | ||
|
|
f1cb5276d7 | ||
|
|
bed1b24340 | ||
|
|
da909ff084 | ||
|
|
8f743b70a4 | ||
|
|
a761017116 | ||
|
|
f06cc25a99 | ||
|
|
eff8480d5c | ||
|
|
eec303de49 | ||
|
|
07a936c5e8 | ||
|
|
a6002a2c6c | ||
|
|
7ba014ba49 | ||
|
|
165c649f31 | ||
|
|
c0a401216b | ||
|
|
2d712a3841 | ||
|
|
a2b5b4dd17 | ||
|
|
4eded82102 | ||
|
|
ec4ff83465 | ||
|
|
8745b8b051 | ||
|
|
4e28c3cadb | ||
|
|
d137735be1 | ||
|
|
1450a21d00 | ||
|
|
4b7a37cf7d | ||
|
|
e012726cd4 | ||
|
|
d3bde0408f | ||
|
|
ec0a0190e7 | ||
|
|
173484c30e | ||
|
|
62e7ab8c2b | ||
|
|
649a553fa4 | ||
|
|
e17a06d7bd | ||
|
|
65560ab65e | ||
|
|
078fc25845 | ||
|
|
67eb4b317a | ||
|
|
843279bacb | ||
|
|
cf35c9dcef | ||
|
|
45ff9394f2 | ||
|
|
282859bc1b | ||
|
|
a3ae97a5a3 | ||
|
|
b9c0f2a3d3 | ||
|
|
0cc07362b8 | ||
|
|
4ee5dcaff3 | ||
|
|
4d44746a88 | ||
|
|
1535bdbcd8 | ||
|
|
ecb160b28d | ||
|
|
7e9941def0 | ||
|
|
f9c6fb30bf | ||
|
|
94ad93ebd7 | ||
|
|
0e1ec5034e | ||
|
|
76b0505bd9 | ||
|
|
b309c1fc91 | ||
|
|
b9a802a19c | ||
|
|
a882ad3e65 | ||
|
|
969105b205 | ||
|
|
28bfb06c67 | ||
|
|
704dcb3e58 | ||
|
|
a7e723d8d4 | ||
|
|
4c6f5096be | ||
|
|
1a1d105fae | ||
|
|
42a0639bb5 | ||
|
|
e27b08203d | ||
|
|
25fc6f1937 | ||
|
|
0ccc3f7252 | ||
|
|
66acd1821d | ||
|
|
93a0217d5f | ||
|
|
711a697570 | ||
|
|
450411a732 | ||
|
|
f80c67828b | ||
|
|
d1889a5e06 | ||
|
|
12263ff02f | ||
|
|
76f914cbc2 | ||
|
|
c1f5252b3f | ||
|
|
20c6f79a44 | ||
|
|
ee6f1405d4 | ||
|
|
ee3541d74e | ||
|
|
e048388213 | ||
|
|
8608ff11f7 | ||
|
|
a14192b7e0 | ||
|
|
601464083b | ||
|
|
ba10657829 | ||
|
|
6315655f93 | ||
|
|
533cb23b73 | ||
|
|
0b95424a0d | ||
|
|
852f252d13 | ||
|
|
080042882f | ||
|
|
dde5b08c47 | ||
|
|
88d2c60ee8 | ||
|
|
eeff5a5d43 | ||
|
|
a0b8cc21cc | ||
|
|
329a7a03a5 | ||
|
|
d0a3503d74 | ||
|
|
bf75f16169 | ||
|
|
4c1ae44bbe | ||
|
|
fc24fa337e | ||
|
|
308784c601 | ||
|
|
9145c9a52c | ||
|
|
db0fe9a755 | ||
|
|
fdf10e3d2e | ||
|
|
ded3e06a9b | ||
|
|
82253219e8 | ||
|
|
eef2112e1e | ||
|
|
b83d56eff2 | ||
|
|
4c539b6db4 | ||
|
|
2ad329e64c | ||
|
|
4de89ec6ce | ||
|
|
fc885c8fa2 | ||
|
|
ef2c98ba39 | ||
|
|
41d922867e | ||
|
|
52e8e7e4b0 | ||
|
|
93f65484de | ||
|
|
7d08cde8b8 | ||
|
|
d3c2bf7dd4 | ||
|
|
5607d2086d | ||
|
|
27ed918896 | ||
|
|
9b2df9cfd9 | ||
|
|
2f8c8c3a28 | ||
|
|
dde7cf18f7 | ||
|
|
48aadbf716 | ||
|
|
5514af60e5 | ||
|
|
e8449eb02f | ||
|
|
9ea21a7ecb | ||
|
|
fec86c2862 | ||
|
|
11e5d4c5b7 | ||
|
|
b852fb0e26 | ||
|
|
0fc18bf3b1 | ||
|
|
d4be584782 | ||
|
|
6f9c08f906 | ||
|
|
70c666c380 | ||
|
|
46197d82b5 | ||
|
|
e4eab17595 | ||
|
|
cf10244f20 | ||
|
|
570684d308 | ||
|
|
244db8d894 | ||
|
|
96d6018ecc | ||
|
|
73ad6dd0c4 | ||
|
|
d0e15da352 | ||
|
|
b1b293a3ec | ||
|
|
5f0660a4e5 | ||
|
|
ec3a3a73ef | ||
|
|
087c4c5929 | ||
|
|
e0ee2e0801 | ||
|
|
c79a9e9858 | ||
|
|
b3fdce36af | ||
|
|
b7535b9526 | ||
|
|
0cf1cc5587 | ||
|
|
098dab889a | ||
|
|
cc856c7cd1 | ||
|
|
a20a155d39 | ||
|
|
1a458fe39f | ||
|
|
051ffdda35 | ||
|
|
a48418364c | ||
|
|
b75497231e | ||
|
|
5cceeb6ef2 | ||
|
|
29b94c5615 | ||
|
|
81fc1eccd3 | ||
|
|
22fae51c9d | ||
|
|
4332f7a640 | ||
|
|
d0e603e62d | ||
|
|
38c28f4358 | ||
|
|
742799b4ac | ||
|
|
5405dc5141 | ||
|
|
013793f989 | ||
|
|
2468cf63cc | ||
|
|
8e9b5d7e50 | ||
|
|
a33515d907 | ||
|
|
a276a33081 | ||
|
|
e325c3dbf6 | ||
|
|
b3fef625a8 | ||
|
|
13994a5f96 | ||
|
|
e8ac62cdd8 | ||
|
|
1bc7134ec2 | ||
|
|
273572f44c | ||
|
|
ac37483d47 | ||
|
|
0cc2b49296 | ||
|
|
44d5adc16f | ||
|
|
f749fc641b | ||
|
|
87c76c09e2 | ||
|
|
e787f9dd9f | ||
|
|
ec7d5b17ab | ||
|
|
47641eee59 | ||
|
|
42abe28cc1 | ||
|
|
b48afa38ac | ||
|
|
f7269d8df2 | ||
|
|
ce0fab8d2f | ||
|
|
33472567de | ||
|
|
f5e12328dc | ||
|
|
486c0f2937 | ||
|
|
e69c4cd1dc | ||
|
|
e0d3ccd8d1 | ||
|
|
16a3deec2c | ||
|
|
92db00692a | ||
|
|
fe88010a72 | ||
|
|
99ae4be2c2 | ||
|
|
a9085752c1 | ||
|
|
e1fbc9255c | ||
|
|
f33e25ac40 | ||
|
|
51365ab006 | ||
|
|
d61b7e48d7 | ||
|
|
f590b24f85 | ||
|
|
308fd55483 | ||
|
|
fefb44e612 | ||
|
|
dd3b562f1a | ||
|
|
30c9aa3df9 | ||
|
|
688184e255 | ||
|
|
75fb7c2e95 | ||
|
|
264b4b669e | ||
|
|
51f12c51ad | ||
|
|
03d134697c | ||
|
|
e706e1a68d | ||
|
|
ff3f42db95 | ||
|
|
e8aedeac36 | ||
|
|
2bf6b263ed | ||
|
|
bf166b757a | ||
|
|
b4165335a7 | ||
|
|
2a1bf2fa71 | ||
|
|
718d50a037 | ||
|
|
41960ada4a | ||
|
|
0a02caad04 | ||
|
|
ff75bdba9e | ||
|
|
189243a9b0 | ||
|
|
34e339c506 | ||
|
|
a437c603c6 | ||
|
|
4e940f7fa0 | ||
|
|
69fde7dead | ||
|
|
4368c182a0 | ||
|
|
48f4b12c0e | ||
|
|
6a6c1b6b21 | ||
|
|
51808d2ad4 | ||
|
|
0735fb1e92 | ||
|
|
850a85915b | ||
|
|
148f1109c6 | ||
|
|
fcbeeca305 | ||
|
|
fb16429867 | ||
|
|
e1439b529b | ||
|
|
db38d7ee78 | ||
|
|
0adcd1b3d9 | ||
|
|
36a984ce3c | ||
|
|
fcefd8031a | ||
|
|
64bebaa1d1 | ||
|
|
0138ff9e16 | ||
|
|
9bc085a23e | ||
|
|
1ce432b136 | ||
|
|
e0fbfa44a4 | ||
|
|
0a92d79ec0 | ||
|
|
b95f6563cc | ||
|
|
547bbecef1 | ||
|
|
700cb3b553 | ||
|
|
8646f8f23a | ||
|
|
99cc69c67d | ||
|
|
2c5d76908a | ||
|
|
832b34fc54 | ||
|
|
000b85ff81 | ||
|
|
bb317d54f2 | ||
|
|
d6f41d8dc0 | ||
|
|
6dff5ed679 | ||
|
|
fb8b6310d5 | ||
|
|
02aaf17ac5 | ||
|
|
b160a8de0b | ||
|
|
e3246fd751 | ||
|
|
91b1775f50 | ||
|
|
fb18ab1ca5 | ||
|
|
01ff55c84e | ||
|
|
523aab2e9e | ||
|
|
9a7d9da654 | ||
|
|
e9446556a1 | ||
|
|
806a5f209f | ||
|
|
c864b368b0 | ||
|
|
27eb09fb19 | ||
|
|
bea7223a0a | ||
|
|
0297823bda | ||
|
|
d1bce5c2d9 | ||
|
|
46c0ae3ccc | ||
|
|
ce6b689147 | ||
|
|
2d92818613 | ||
|
|
487878819e | ||
|
|
6682b1ced1 | ||
|
|
bc89b0658a | ||
|
|
d9dde36c74 | ||
|
|
44284ea5fb | ||
|
|
9f0c0b34af | ||
|
|
87ec44aed5 | ||
|
|
898e6b4f80 | ||
|
|
dea2600913 | ||
|
|
dc46ad16ae | ||
|
|
456550a943 | ||
|
|
7b789b3701 | ||
|
|
0480d493cf | ||
|
|
b4d7733e0a | ||
|
|
8b8fe7a0ae | ||
|
|
85ffc90f66 | ||
|
|
fa38798066 | ||
|
|
d657330584 | ||
|
|
36cb79de62 | ||
|
|
063ee5e855 | ||
|
|
7393b69757 | ||
|
|
8bd1903d98 | ||
|
|
d8bf540db2 | ||
|
|
e29f17ac46 | ||
|
|
1770f3fd0d | ||
|
|
4239f2ed71 | ||
|
|
3939ec28ba | ||
|
|
742ec2b38d | ||
|
|
9296d35517 | ||
|
|
06c15a792e | ||
|
|
95ca1e6a9d | ||
|
|
1df82110d1 | ||
|
|
24c743d23d | ||
|
|
f1b1ebe29e | ||
|
|
6384cdc74d | ||
|
|
d093fbb069 | ||
|
|
f9b4505843 | ||
|
|
ca2bcc647d | ||
|
|
b90048d72e | ||
|
|
a5fe08e84c | ||
|
|
2874cf531c | ||
|
|
ea7058e896 | ||
|
|
38bb10d36b | ||
|
|
58943bb156 | ||
|
|
2d66da3cb9 | ||
|
|
b7efbf9040 | ||
|
|
5222e157cb | ||
|
|
1e3a948977 | ||
|
|
20cc5107da | ||
|
|
a6a4d5f09b | ||
|
|
028c53287d | ||
|
|
24a9bea137 | ||
|
|
ef1a2d1730 | ||
|
|
6abff36e07 | ||
|
|
902685a197 | ||
|
|
c04b146486 | ||
|
|
7bb5afa585 | ||
|
|
06fde4fcd0 | ||
|
|
195845ab0c | ||
|
|
81a329f1e7 | ||
|
|
cd6272a1c9 | ||
|
|
53be752787 | ||
|
|
42ac06c114 | ||
|
|
9e159ed5ab | ||
|
|
2c42972230 | ||
|
|
f926e58891 | ||
|
|
a784c6bd52 | ||
|
|
05d78f5cb5 | ||
|
|
4ef7615d88 | ||
|
|
77c2783a3e | ||
|
|
ce4f1258b5 | ||
|
|
3fbaba6693 | ||
|
|
a8b36aed92 | ||
|
|
4749eccfa5 | ||
|
|
5b1dfc123f | ||
|
|
41a3623fcc | ||
|
|
296ac203d4 | ||
|
|
0c9436ca82 | ||
|
|
70c9dd1b95 | ||
|
|
80753b1115 | ||
|
|
a194216568 | ||
|
|
8bee424cc0 | ||
|
|
25ab3cabfe | ||
|
|
587174b771 | ||
|
|
3e1c34efe6 | ||
|
|
5864637f1c | ||
|
|
ec6b346ca1 | ||
|
|
e99f5bcced | ||
|
|
b89309ab82 | ||
|
|
2d230ec96a | ||
|
|
4550333f1e | ||
|
|
3ba610eb64 | ||
|
|
2436c6a118 | ||
|
|
4de80b26c1 | ||
|
|
bab14a1fbf | ||
|
|
0c27ff11b9 | ||
|
|
734e2ffbb2 | ||
|
|
da42c51af2 | ||
|
|
7cb6801241 | ||
|
|
f6c04b9b84 | ||
|
|
4eacb21259 | ||
|
|
6d1a3ccdcc | ||
|
|
c870f6e87d | ||
|
|
6d907094d7 | ||
|
|
6643f0d1e0 | ||
|
|
20b07e0752 | ||
|
|
092423adc7 | ||
|
|
a50ca1a85f | ||
|
|
02199c8c1d | ||
|
|
c166c92685 | ||
|
|
f243515261 | ||
|
|
98181fe21c | ||
|
|
a26ce8d8b5 | ||
|
|
de0e27c512 | ||
|
|
09e7d76c6f | ||
|
|
d597e05fa9 | ||
|
|
98dc991339 | ||
|
|
1d40434d2b | ||
|
|
46b87dc7eb | ||
|
|
fe7c56d269 | ||
|
|
5ba4801a79 | ||
|
|
ad564e25ca | ||
|
|
404be948d4 | ||
|
|
8cbc345f36 | ||
|
|
89927fd7e9 | ||
|
|
6b4a024234 | ||
|
|
18794831e0 | ||
|
|
3fb851304f | ||
|
|
d267338837 | ||
|
|
8e1641dac9 | ||
|
|
57d37ffba8 | ||
|
|
82afa81220 | ||
|
|
d730eb8d31 | ||
|
|
5a219b580f | ||
|
|
fb83bfb363 | ||
|
|
df7d3d18b6 | ||
|
|
a0535aa3db | ||
|
|
202b6121ab | ||
|
|
4e8b814ec2 | ||
|
|
6becca17bf | ||
|
|
c8b64d4162 | ||
|
|
0854303710 | ||
|
|
cba3e62e71 | ||
|
|
dcba720e97 | ||
|
|
6c614c06f6 | ||
|
|
9a812f11e7 | ||
|
|
917132fe26 | ||
|
|
187ca5dc8f | ||
|
|
7d795771d3 | ||
|
|
e51bc4ea78 | ||
|
|
2dc3235d4f | ||
|
|
040bb4a5a8 | ||
|
|
fc4436f091 | ||
|
|
5dbdef25d3 | ||
|
|
b9f3ac2eea | ||
|
|
9fc0c3b3de | ||
|
|
4f81184da0 | ||
|
|
8223561844 | ||
|
|
32a6beae65 | ||
|
|
146068c936 | ||
|
|
3b8c5ef21a | ||
|
|
e60ef8fc97 | ||
|
|
24d755b123 | ||
|
|
7c89f0b5b9 | ||
|
|
134a10f56c | ||
|
|
3a70c86f49 | ||
|
|
ac431bbc4a | ||
|
|
371097eb4d | ||
|
|
6346059698 | ||
|
|
372c284ad4 | ||
|
|
264ccdbf6d | ||
|
|
b95150ff11 | ||
|
|
81fc04f24d | ||
|
|
3d4ebddb04 | ||
|
|
de299e0d6a | ||
|
|
29cb8bfec4 | ||
|
|
b7f3e00fbf | ||
|
|
27a18d60a7 | ||
|
|
de8f6d3e8d | ||
|
|
6893635251 | ||
|
|
94a38a3b47 | ||
|
|
7d99e21d0d | ||
|
|
df3eb40e3c | ||
|
|
ba6b5f8fd1 | ||
|
|
8f518993a4 | ||
|
|
dac48a2610 | ||
|
|
7c0d10da79 | ||
|
|
d77b52af96 | ||
|
|
29f6463ed9 | ||
|
|
ed0bdbf31d | ||
|
|
b152d3e06d | ||
|
|
16cd57fe55 | ||
|
|
3f578122a3 | ||
|
|
51a27322be | ||
|
|
050feed5dc | ||
|
|
e3ddc16657 | ||
|
|
33cdf20cd5 | ||
|
|
317e59df4b | ||
|
|
a9a6f5b97e | ||
|
|
8b1444ebc2 | ||
|
|
509071949a | ||
|
|
697d857549 | ||
|
|
2ea45b1fdc | ||
|
|
726595e117 | ||
|
|
1666e32aaf | ||
|
|
6a69bbe4b5 | ||
|
|
7a608b4fb0 | ||
|
|
814ad87a42 | ||
|
|
3e4b5e23fa | ||
|
|
ab24ed8088 | ||
|
|
50ba2e329a | ||
|
|
c1e2a98f46 | ||
|
|
e04aa80fd6 | ||
|
|
482e977af4 | ||
|
|
2535bbbcf1 | ||
|
|
6698773d81 | ||
|
|
aefaf47f4c | ||
|
|
9b49125776 | ||
|
|
b33a2ac90d | ||
|
|
f67953c447 | ||
|
|
981632f599 | ||
|
|
a6c453d826 | ||
|
|
4087e685f4 | ||
|
|
5255085de1 | ||
|
|
9247ded710 | ||
|
|
0bb0cbaef0 | ||
|
|
0f7d272e13 | ||
|
|
00dafe3121 | ||
|
|
e44494aad0 | ||
|
|
4ab3dc2599 | ||
|
|
acfad7a982 | ||
|
|
b29b5b7ac1 | ||
|
|
7803ffb995 | ||
|
|
fc79cdfaa2 | ||
|
|
f9dbc6bc78 | ||
|
|
dc7aaae235 | ||
|
|
9804a98af8 | ||
|
|
647e954e8a | ||
|
|
004d9118bc | ||
|
|
594c8aad91 | ||
|
|
542a0008c9 | ||
|
|
24f7918aa4 | ||
|
|
2eec329bdf | ||
|
|
0411d4a8c9 | ||
|
|
a986faea56 | ||
|
|
ad71d0a03f | ||
|
|
0955c6d6fb | ||
|
|
d30b44ee0f | ||
|
|
a6f4db0f25 | ||
|
|
4547c328bc | ||
|
|
5027aeb3a0 | ||
|
|
c0239a659c | ||
|
|
e404da4192 | ||
|
|
69fa7d0091 | ||
|
|
e1d6aec682 | ||
|
|
155795a18e | ||
|
|
8c4052e884 | ||
|
|
9b119fa724 | ||
|
|
a17c1c063e | ||
|
|
cd9bb56db5 | ||
|
|
8150f934fd | ||
|
|
4c8f3f7bae | ||
|
|
ab5f176f58 | ||
|
|
3e404101bd | ||
|
|
65105d9dbe | ||
|
|
3a4d351a57 | ||
|
|
973f555544 | ||
|
|
56964a890b | ||
|
|
cef41661dd | ||
|
|
79a9ef4859 | ||
|
|
2798dd5916 | ||
|
|
8143c16c14 | ||
|
|
42435ab34a | ||
|
|
661ed17d23 | ||
|
|
218e35e3aa | ||
|
|
cabad83418 | ||
|
|
87c415830f | ||
|
|
48495f0d66 | ||
|
|
d9adb4fc94 | ||
|
|
1da4efec86 | ||
|
|
1c630eb604 | ||
|
|
1c18a788f4 | ||
|
|
5887f0fe6b | ||
|
|
b782489a8c | ||
|
|
01381488f4 | ||
|
|
6555d5869f | ||
|
|
54c4f40188 | ||
|
|
62e8bee2a8 | ||
|
|
9ec3ddd492 | ||
|
|
d81dbb13e4 | ||
|
|
c238367b64 | ||
|
|
cdcb8a50d1 | ||
|
|
b831b9d6b2 | ||
|
|
bbe91f439a | ||
|
|
288944db2c | ||
|
|
f2c07d8f81 | ||
|
|
d6a9746824 | ||
|
|
f84274f1c5 | ||
|
|
2118d920f5 | ||
|
|
207004beff | ||
|
|
27d084ce39 | ||
|
|
f705889c23 | ||
|
|
040d7d9ae3 | ||
|
|
f9b1e84704 | ||
|
|
55c0bb6d34 | ||
|
|
2b55b9b250 | ||
|
|
fffa2d5a1b | ||
|
|
0926ae530c | ||
|
|
0b709f7dfb | ||
|
|
b5da2c4199 | ||
|
|
9ede01f130 | ||
|
|
55b54de6a0 | ||
|
|
5357867103 | ||
|
|
38a255e069 | ||
|
|
11c8b47c39 | ||
|
|
00a29f3d88 | ||
|
|
e5b9da5201 | ||
|
|
3d0beba261 | ||
|
|
147947662c | ||
|
|
8e8486497f |
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: Bug/Problem report
|
||||||
|
about: Create a report to help us improve Calibre-Web
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
<!-- Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md) -->
|
||||||
|
|
||||||
|
**Describe the bug/problem**
|
||||||
|
A clear and concise description of what the bug is. If you are asking for support, please check our [Wiki](https://github.com/janeczku/calibre-web/wiki) if your question is already answered there.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Logfile**
|
||||||
|
Add content of calibre-web.log file or the relevant error, try to reproduce your problem with "debug" log-level to get more output.
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Environment (please complete the following information):**
|
||||||
|
- OS: [e.g. Windows 10/Raspberry Pi OS]
|
||||||
|
- Python version: [e.g. python2.7]
|
||||||
|
- Calibre-Web version: [e.g. 0.6.8 or 087c4c59 (git rev-parse --short HEAD)]:
|
||||||
|
- Docker container: [None/Technosoft2000/Linuxuser]:
|
||||||
|
- Special Hardware: [e.g. Rasperry Pi Zero]
|
||||||
|
- Browser: [e.g. Chrome 83.0.4103.97, Safari 13.3.7, Firefox 68.0.1 ESR]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here. [e.g. access via reverse proxy, database background sync, special database location]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for Calibre-Web
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md) -->
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
+1
-3
@@ -21,14 +21,12 @@ vendor/
|
|||||||
# calibre-web
|
# calibre-web
|
||||||
*.db
|
*.db
|
||||||
*.log
|
*.log
|
||||||
config.ini
|
|
||||||
cps/static/[0-9]*
|
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
*.bak
|
*.bak
|
||||||
*.log.*
|
*.log.*
|
||||||
tags
|
|
||||||
|
|
||||||
settings.yaml
|
settings.yaml
|
||||||
gdrive_credentials
|
gdrive_credentials
|
||||||
client_secrets.json
|
client_secrets.json
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
## How to contribute to Calibre-Web
|
||||||
|
|
||||||
|
First of all, we would like to thank you for reading this text. We are happy you are willing to contribute to Calibre-Web.
|
||||||
|
|
||||||
|
### **General**
|
||||||
|
|
||||||
|
**Communication language** is English. Google translated texts are not as bad as you might think, they are usually understandable, so don't worry if you generate your post that way.
|
||||||
|
|
||||||
|
**Calibre-Web** is not **Calibre**. If you are having a question regarding Calibre please post this at their [repository](https://github.com/kovidgoyal/calibre).
|
||||||
|
|
||||||
|
**Docker-Containers** of Calibre-Web are maintained by different persons than the people who drive Calibre-Web. If we come to the conclusion during our analysis that the problem is related to Docker, we might ask you to open a new issue at the repository of the Docker Container.
|
||||||
|
|
||||||
|
If you are having **Basic Installation Problems** with python or its dependencies, please consider using your favorite search engine to find a solution. In case you can't find a solution, we are happy to help you.
|
||||||
|
|
||||||
|
We can offer only very limited support regarding configuration of **Reverse-Proxy Installations**, **OPDS-Reader** or other programs in combination with Calibre-Web.
|
||||||
|
|
||||||
|
### **Translation**
|
||||||
|
|
||||||
|
Some of the user languages in Calibre-Web having missing translations. We are happy to add the missing texts if you translate them. Create a Pull Request, create an issue with the .po file attached, or write an email to "ozzie.fernandez.isaacs@googlemail.com" with attached translation file. To display all book languages in your native language an additional file is used (iso_language_names.py). The content of this file is auto-generated with the corresponding translations of Calibre, please do not edit this file on your own.
|
||||||
|
|
||||||
|
### **Documentation**
|
||||||
|
|
||||||
|
The Calibre-Web documentation is hosted in the Github [Wiki](https://github.com/janeczku/calibre-web/wiki). The Wiki is open to everybody, if you find a problem, feel free to correct it. If information is missing, you are welcome to add it. The content will be reviewed time by time. Please try to be consistent with the form with the other Wiki pages (e.g. the project name is Calibre-Web with 2 capital letters and a dash in between).
|
||||||
|
|
||||||
|
### **Reporting a bug**
|
||||||
|
|
||||||
|
Do not open up a GitHub issue if the bug is a **security vulnerability** in Calibre-Web. Instead, please write an email to "ozzie.fernandez.isaacs@googlemail.com".
|
||||||
|
|
||||||
|
Ensure the ***bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
|
||||||
|
|
||||||
|
If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=bug_report.md&title=). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you providing the right information. Deleting the form and just pasting the stack trace doesn't speed up fixing the problem. If your issue could be resolved, consider closing the issue.
|
||||||
|
|
||||||
|
### **Feature Request**
|
||||||
|
|
||||||
|
If there is a feature missing in Calibre-Web and you can't find a feature request in the [Issues](https://github.com/janeczku/calibre-web/issues) section, you could create a [feature request](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=feature_request.md&title=).
|
||||||
|
We will not extend Calibre-Web with any more login abilities or add further files storages, or file syncing ability. Furthermore Calibre-Web is made for home usage for company in-house usage, so requests regarding any sorts of social interaction capability, payment routines, search engine or web site analytics integration will not be implemented.
|
||||||
|
|
||||||
|
### **Contributing code to Calibre-Web**
|
||||||
|
|
||||||
|
Open a new GitHub pull request with the patch. Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
@@ -12,7 +12,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, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, ukrainian
|
- User Interface in czech, dutch, english, finnish, french, german, hungarian, italian, japanese, khmer, polish, russian, simplified 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)
|
||||||
@@ -21,16 +21,18 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
|||||||
- Restrict eBook download to logged-in users
|
- Restrict eBook download to logged-in users
|
||||||
- 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
|
||||||
- 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)
|
||||||
- Upload new books in many formats
|
- 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 for certain users
|
- Ability to hide content based on categories and Custom Column content per user
|
||||||
- Self-update capability
|
- Self-update capability
|
||||||
- "Magic Link" login to make it easy to log on eReaders
|
- "Magic Link" login to make it easy to log on eReaders
|
||||||
|
- Login via LDAP, google/github oauth and via proxy authentication
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt`.
|
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x) or `pip install --target vendor -r requirements.txt` (python2.7).
|
||||||
2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
|
2. Execute the command: `python cps.py` (or `nohup python 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
|
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\
|
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
|
||||||
@@ -52,23 +54,21 @@ Optionally, to enable on-the-fly conversion from one ebook format to another whe
|
|||||||
|
|
||||||
[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page.
|
[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page.
|
||||||
|
|
||||||
\*** DEPRECATED \*** Support will be removed in future releases
|
[Download](https://github.com/geek1011/kepubify/releases/tag/v3.1.2) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `\opt\kepubify` Windows: `C:\Program Files\kepubify`.
|
||||||
|
|
||||||
[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named `kindlegen` in the `vendor` folder.
|
|
||||||
|
|
||||||
## Docker Images
|
## Docker Images
|
||||||
|
|
||||||
Pre-built Docker images are available in these Docker Hub repositories:
|
Pre-built Docker images are available in these Docker Hub repositories:
|
||||||
|
|
||||||
#### **Technosoft2000 - x64**
|
#### **Technosoft2000 - x64**
|
||||||
+ Docker Hub - [https://hub.docker.com/r/technosoft2000/calibre-web/](https://hub.docker.com/r/technosoft2000/calibre-web/)
|
+ 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)
|
+ Github - [https://github.com/Technosoft2000/docker-calibre-web](https://github.com/Technosoft2000/docker-calibre-web)
|
||||||
|
|
||||||
Includes the Calibre `ebook-convert` binary.
|
Includes the Calibre `ebook-convert` binary.
|
||||||
+ The "path to convertertool" should be set to `/opt/calibre/ebook-convert`
|
+ 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)
|
||||||
+ Github - [https://github.com/linuxserver/docker-calibre-web](https://github.com/linuxserver/docker-calibre-web)
|
+ Github - [https://github.com/linuxserver/docker-calibre-web](https://github.com/linuxserver/docker-calibre-web)
|
||||||
+ Github - (Optional Calibre layer) - [https://github.com/linuxserver/docker-calibre-web/tree/calibre](https://github.com/linuxserver/docker-calibre-web/tree/calibre)
|
+ Github - (Optional Calibre layer) - [https://github.com/linuxserver/docker-calibre-web/tree/calibre](https://github.com/linuxserver/docker-calibre-web/tree/calibre)
|
||||||
|
|
||||||
@@ -83,3 +83,7 @@ Pre-built Docker images are available in these Docker Hub repositories:
|
|||||||
# Wiki
|
# Wiki
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
# Contributing to Calibre-Web
|
||||||
|
|
||||||
|
Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ from cps.shelf import shelf
|
|||||||
from cps.admin import admi
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cps.kobo import kobo, get_kobo_activated
|
||||||
|
from cps.kobo_auth import kobo_auth
|
||||||
|
kobo_available = get_kobo_activated()
|
||||||
|
except ImportError:
|
||||||
|
kobo_available = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cps.oauth_bb import oauth
|
from cps.oauth_bb import oauth
|
||||||
oauth_available = True
|
oauth_available = True
|
||||||
@@ -58,6 +66,9 @@ def main():
|
|||||||
app.register_blueprint(admi)
|
app.register_blueprint(admi)
|
||||||
app.register_blueprint(gdrive)
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
|
if kobo_available:
|
||||||
|
app.register_blueprint(kobo)
|
||||||
|
app.register_blueprint(kobo_auth)
|
||||||
if oauth_available:
|
if oauth_available:
|
||||||
app.register_blueprint(oauth)
|
app.register_blueprint(oauth)
|
||||||
success = web_server.start()
|
success = web_server.start()
|
||||||
|
|||||||
Executable → Regular
+18
-11
@@ -33,7 +33,7 @@ from flask_login import LoginManager
|
|||||||
from flask_babel import Babel
|
from flask_babel import Babel
|
||||||
from flask_principal import Principal
|
from flask_principal import Principal
|
||||||
|
|
||||||
from . import logger, cache_buster, cli, config_sql, ub, db, services
|
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
|
||||||
|
|
||||||
@@ -57,11 +57,17 @@ mimetypes.add_type('application/ogg', '.ogg')
|
|||||||
mimetypes.add_type('application/ogg', '.oga')
|
mimetypes.add_type('application/ogg', '.oga')
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.config.update(
|
||||||
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
|
SESSION_COOKIE_SAMESITE='Lax',
|
||||||
|
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
lm = LoginManager()
|
lm = LoginManager()
|
||||||
lm.login_view = 'web.login'
|
lm.login_view = 'web.login'
|
||||||
lm.anonymous_user = ub.Anonymous
|
lm.anonymous_user = ub.Anonymous
|
||||||
|
lm.session_protection = 'strong'
|
||||||
|
|
||||||
ub.init_db(cli.settingspath)
|
ub.init_db(cli.settingspath)
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
@@ -75,6 +81,9 @@ _BABEL_TRANSLATIONS = set()
|
|||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
from . import services
|
||||||
|
|
||||||
|
calibre_db = db.CalibreDB()
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
@@ -89,10 +98,11 @@ def create_app():
|
|||||||
log.info('Starting Calibre Web...')
|
log.info('Starting Calibre Web...')
|
||||||
Principal(app)
|
Principal(app)
|
||||||
lm.init_app(app)
|
lm.init_app(app)
|
||||||
app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT')
|
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
||||||
|
|
||||||
web_server.init_app(app, config)
|
web_server.init_app(app, config)
|
||||||
db.setup_db(config)
|
calibre_db.setup_db(config, cli.settingspath)
|
||||||
|
calibre_db.start()
|
||||||
|
|
||||||
babel.init_app(app)
|
babel.init_app(app)
|
||||||
_BABEL_TRANSLATIONS.update(str(item) for item in babel.list_translations())
|
_BABEL_TRANSLATIONS.update(str(item) for item in babel.list_translations())
|
||||||
@@ -116,14 +126,13 @@ def get_locale():
|
|||||||
if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
|
if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
|
||||||
return user.locale
|
return user.locale
|
||||||
|
|
||||||
preferred = set()
|
preferred = list()
|
||||||
if request.accept_languages:
|
if request.accept_languages:
|
||||||
for x in request.accept_languages.values():
|
for x in request.accept_languages.values():
|
||||||
try:
|
try:
|
||||||
preferred.add(str(LC.parse(x.replace('-', '_'))))
|
preferred.append(str(LC.parse(x.replace('-', '_'))))
|
||||||
except (UnknownLocaleError, ValueError) as e:
|
except (UnknownLocaleError, ValueError) as e:
|
||||||
log.warning('Could not parse locale "%s": %s', x, e)
|
log.debug('Could not parse locale "%s": %s', x, e)
|
||||||
# preferred.append('en')
|
|
||||||
|
|
||||||
return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS)
|
return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS)
|
||||||
|
|
||||||
@@ -135,6 +144,4 @@ def get_timezone():
|
|||||||
|
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
|
updater_thread.start()
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
|
|||||||
+22
-10
@@ -30,7 +30,7 @@ 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 _
|
||||||
|
|
||||||
from . import db, converter, uploader, server, isoLanguages
|
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
||||||
from .web import render_title_template
|
from .web import render_title_template
|
||||||
try:
|
try:
|
||||||
from flask_login import __version__ as flask_loginVersion
|
from flask_login import __version__ as flask_loginVersion
|
||||||
@@ -43,14 +43,22 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
unidecode_version = _(u'not installed')
|
unidecode_version = _(u'not installed')
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flask_dance import __version__ as flask_danceVersion
|
||||||
|
except ImportError:
|
||||||
|
flask_danceVersion = None
|
||||||
|
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
about = flask.Blueprint('about', __name__)
|
about = flask.Blueprint('about', __name__)
|
||||||
|
|
||||||
|
|
||||||
_VERSIONS = OrderedDict(
|
_VERSIONS = OrderedDict(
|
||||||
Platform = ' '.join(platform.uname()),
|
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||||
Python=sys.version,
|
Python=sys.version,
|
||||||
|
Calibre_Web=constants.STABLE_VERSION['version'] + ' - '
|
||||||
|
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
|
||||||
|
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
|
||||||
WebServer=server.VERSION,
|
WebServer=server.VERSION,
|
||||||
Flask=flask.__version__,
|
Flask=flask.__version__,
|
||||||
Flask_Login=flask_loginVersion,
|
Flask_Login=flask_loginVersion,
|
||||||
@@ -65,9 +73,11 @@ _VERSIONS = OrderedDict(
|
|||||||
iso639=isoLanguages.__version__,
|
iso639=isoLanguages.__version__,
|
||||||
pytz=pytz.__version__,
|
pytz=pytz.__version__,
|
||||||
Unidecode = unidecode_version,
|
Unidecode = unidecode_version,
|
||||||
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else u'not installed',
|
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None,
|
||||||
Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed',
|
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
|
||||||
|
Goodreads = u'installed' if bool(services.goodreads_support) else None,
|
||||||
|
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||||
|
flask_dance = flask_danceVersion
|
||||||
)
|
)
|
||||||
_VERSIONS.update(uploader.get_versions())
|
_VERSIONS.update(uploader.get_versions())
|
||||||
|
|
||||||
@@ -75,10 +85,12 @@ _VERSIONS.update(uploader.get_versions())
|
|||||||
@about.route("/stats")
|
@about.route("/stats")
|
||||||
@flask_login.login_required
|
@flask_login.login_required
|
||||||
def stats():
|
def stats():
|
||||||
counter = db.session.query(db.Books).count()
|
counter = calibre_db.session.query(db.Books).count()
|
||||||
authors = db.session.query(db.Authors).count()
|
authors = calibre_db.session.query(db.Authors).count()
|
||||||
categorys = db.session.query(db.Tags).count()
|
categorys = calibre_db.session.query(db.Tags).count()
|
||||||
series = db.session.query(db.Series).count()
|
series = calibre_db.session.query(db.Series).count()
|
||||||
_VERSIONS['ebook converter'] = _(converter.get_version())
|
_VERSIONS['ebook converter'] = _(converter.get_calibre_version())
|
||||||
|
_VERSIONS['unrar'] = _(converter.get_unrar_version())
|
||||||
|
_VERSIONS['kepubify'] = _(converter.get_kepubify_version())
|
||||||
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=_VERSIONS,
|
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=_VERSIONS,
|
||||||
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
|
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
|
||||||
|
|||||||
+567
-219
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
# Copyright (C) 2016-2019 jkrehm andy29485 OzzieIsaacs
|
# Copyright (C) 2016-2019 jkrehm andy29485 OzzieIsaacs
|
||||||
#
|
#
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ parser.add_argument('-k', metavar='path',
|
|||||||
help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile')
|
help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile')
|
||||||
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-web',
|
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-web',
|
||||||
version=version_info())
|
version=version_info())
|
||||||
parser.add_argument('-i', metavar='ip-adress', help='Server IP-Adress 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')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||
Executable → Regular
+66
-31
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -19,10 +18,17 @@
|
|||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
from __future__ import division, print_function, unicode_literals
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
|
||||||
from . import logger, isoLanguages
|
from . import logger, isoLanguages
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
use_PIL = True
|
||||||
|
except ImportError as e:
|
||||||
|
use_PIL = False
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -30,22 +36,53 @@ log = logger.create()
|
|||||||
try:
|
try:
|
||||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||||
use_comic_meta = True
|
use_comic_meta = True
|
||||||
except ImportError as e:
|
try:
|
||||||
log.debug('cannot import comicapi, extracting comic metadata will not work: %s', e)
|
from comicapi import __version__ as comic_version
|
||||||
|
except (ImportError):
|
||||||
|
comic_version = ''
|
||||||
|
except (ImportError, LookupError) as e:
|
||||||
|
log.debug('Cannot import comicapi, extracting comic metadata will not work: %s', e)
|
||||||
import zipfile
|
import zipfile
|
||||||
import tarfile
|
import tarfile
|
||||||
|
try:
|
||||||
|
import rarfile
|
||||||
|
use_rarfile = True
|
||||||
|
except ImportError as e:
|
||||||
|
log.debug('Cannot import rarfile, extracting cover files from rar files will not work: %s', e)
|
||||||
|
use_rarfile = False
|
||||||
use_comic_meta = False
|
use_comic_meta = False
|
||||||
|
|
||||||
|
def _cover_processing(tmp_file_name, img, extension):
|
||||||
|
if use_PIL:
|
||||||
|
# convert to jpg because calibre only supports jpg
|
||||||
|
if extension in ('.png', '.webp'):
|
||||||
|
imgc = PILImage.open(io.BytesIO(img))
|
||||||
|
im = imgc.convert('RGB')
|
||||||
|
tmp_bytesio = io.BytesIO()
|
||||||
|
im.save(tmp_bytesio, format='JPEG')
|
||||||
|
img = tmp_bytesio.getvalue()
|
||||||
|
|
||||||
def extractCover(tmp_file_name, original_file_extension):
|
prefix = os.path.dirname(tmp_file_name)
|
||||||
|
if img:
|
||||||
|
tmp_cover_name = prefix + '/cover.jpg'
|
||||||
|
image = open(tmp_cover_name, 'wb')
|
||||||
|
image.write(img)
|
||||||
|
image.close()
|
||||||
|
else:
|
||||||
|
tmp_cover_name = None
|
||||||
|
return tmp_cover_name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||||
|
cover_data = extension = None
|
||||||
if use_comic_meta:
|
if use_comic_meta:
|
||||||
archive = ComicArchive(tmp_file_name)
|
archive = ComicArchive(tmp_file_name, rar_exe_path=rarExecutable)
|
||||||
cover_data = None
|
|
||||||
for index, name in enumerate(archive.getPageNameList()):
|
for index, name in enumerate(archive.getPageNameList()):
|
||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension == '.jpg' or extension == '.jpeg':
|
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||||
cover_data = archive.getPage(index)
|
cover_data = archive.getPage(index)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -55,7 +92,7 @@ def extractCover(tmp_file_name, original_file_extension):
|
|||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension == '.jpg' or extension == '.jpeg':
|
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||||
cover_data = cf.read(name)
|
cover_data = cf.read(name)
|
||||||
break
|
break
|
||||||
elif original_file_extension.upper() == '.CBT':
|
elif original_file_extension.upper() == '.CBT':
|
||||||
@@ -64,23 +101,28 @@ def extractCover(tmp_file_name, original_file_extension):
|
|||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension == '.jpg' or extension == '.jpeg':
|
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||||
cover_data = cf.extractfile(name).read()
|
cover_data = cf.extractfile(name).read()
|
||||||
break
|
break
|
||||||
prefix = os.path.dirname(tmp_file_name)
|
elif original_file_extension.upper() == '.CBR' and use_rarfile:
|
||||||
if cover_data:
|
try:
|
||||||
tmp_cover_name = prefix + '/cover' + extension
|
rarfile.UNRAR_TOOL = rarExecutable
|
||||||
image = open(tmp_cover_name, 'wb')
|
cf = rarfile.RarFile(tmp_file_name)
|
||||||
image.write(cover_data)
|
for name in cf.getnames():
|
||||||
image.close()
|
ext = os.path.splitext(name)
|
||||||
else:
|
if len(ext) > 1:
|
||||||
tmp_cover_name = None
|
extension = ext[1].lower()
|
||||||
return tmp_cover_name
|
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||||
|
cover_data = cf.read(name)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
log.debug('Rarfile failed with error: %s', e)
|
||||||
|
return _cover_processing(tmp_file_name, cover_data, extension)
|
||||||
|
|
||||||
|
|
||||||
def get_comic_info(tmp_file_path, original_file_name, original_file_extension):
|
def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rarExecutable):
|
||||||
if use_comic_meta:
|
if use_comic_meta:
|
||||||
archive = ComicArchive(tmp_file_path)
|
archive = ComicArchive(tmp_file_path, rar_exe_path=rarExecutable)
|
||||||
if archive.seemsToBeAComicArchive():
|
if archive.seemsToBeAComicArchive():
|
||||||
if archive.hasMetadata(MetaDataStyle.CIX):
|
if archive.hasMetadata(MetaDataStyle.CIX):
|
||||||
style = MetaDataStyle.CIX
|
style = MetaDataStyle.CIX
|
||||||
@@ -92,34 +134,27 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
# if style is not None:
|
# if style is not None:
|
||||||
loadedMetadata = archive.readMetadata(style)
|
loadedMetadata = archive.readMetadata(style)
|
||||||
|
|
||||||
lang = loadedMetadata.language
|
lang = loadedMetadata.language or ""
|
||||||
if lang:
|
loadedMetadata.language = isoLanguages.get_lang3(lang)
|
||||||
if len(lang) == 2:
|
|
||||||
loadedMetadata.language = isoLanguages.get(part1=lang).name
|
|
||||||
elif len(lang) == 3:
|
|
||||||
loadedMetadata.language = isoLanguages.get(part3=lang).name
|
|
||||||
else:
|
|
||||||
loadedMetadata.language = ""
|
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
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),
|
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)
|
||||||
else:
|
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
extension=original_file_extension,
|
extension=original_file_extension,
|
||||||
title=original_file_name,
|
title=original_file_name,
|
||||||
author=u'Unknown',
|
author=u'Unknown',
|
||||||
cover=extractCover(tmp_file_path, original_file_extension),
|
cover=_extractCover(tmp_file_path, original_file_extension, rarExecutable),
|
||||||
description="",
|
description="",
|
||||||
tags="",
|
tags="",
|
||||||
series="",
|
series="",
|
||||||
|
|||||||
+129
-38
@@ -22,15 +22,24 @@ import os
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean
|
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
from . import constants, cli, logger
|
from . import constants, cli, logger, ub
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
_Base = declarative_base()
|
_Base = declarative_base()
|
||||||
|
|
||||||
|
class _Flask_Settings(_Base):
|
||||||
|
__tablename__ = 'flask_settings'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
flask_session_key = Column(BLOB, default="")
|
||||||
|
|
||||||
|
def __init__(self, key):
|
||||||
|
self.flask_session_key = key
|
||||||
|
|
||||||
|
|
||||||
# Baseclass for representing settings in app.db with email server settings and Calibre database settings
|
# Baseclass for representing settings in app.db with email server settings and Calibre database settings
|
||||||
# (application settings)
|
# (application settings)
|
||||||
@@ -44,9 +53,11 @@ class _Settings(_Base):
|
|||||||
mail_login = Column(String, default='mail@example.com')
|
mail_login = Column(String, default='mail@example.com')
|
||||||
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)
|
||||||
|
|
||||||
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_certfile = Column(String)
|
config_certfile = Column(String)
|
||||||
config_keyfile = Column(String)
|
config_keyfile = Column(String)
|
||||||
|
|
||||||
@@ -68,41 +79,49 @@ class _Settings(_Base):
|
|||||||
config_anonbrowse = Column(SmallInteger, default=0)
|
config_anonbrowse = Column(SmallInteger, default=0)
|
||||||
config_public_reg = Column(SmallInteger, default=0)
|
config_public_reg = Column(SmallInteger, default=0)
|
||||||
config_remote_login = Column(Boolean, default=False)
|
config_remote_login = Column(Boolean, default=False)
|
||||||
|
config_kobo_sync = Column(Boolean, default=False)
|
||||||
|
|
||||||
config_default_role = Column(SmallInteger, default=0)
|
config_default_role = Column(SmallInteger, default=0)
|
||||||
config_default_show = Column(SmallInteger, default=6143)
|
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
|
||||||
config_columns_to_ignore = Column(String)
|
config_columns_to_ignore = Column(String)
|
||||||
|
|
||||||
|
config_denied_tags = Column(String, default="")
|
||||||
|
config_allowed_tags = Column(String, default="")
|
||||||
|
config_restricted_column = Column(SmallInteger, default=0)
|
||||||
|
config_denied_column_value = Column(String, default="")
|
||||||
|
config_allowed_column_value = Column(String, default="")
|
||||||
|
|
||||||
config_use_google_drive = Column(Boolean, default=False)
|
config_use_google_drive = Column(Boolean, default=False)
|
||||||
config_google_drive_folder = Column(String)
|
config_google_drive_folder = Column(String)
|
||||||
config_google_drive_watch_changes_response = Column(String)
|
config_google_drive_watch_changes_response = Column(JSON, default={})
|
||||||
|
|
||||||
config_use_goodreads = Column(Boolean, default=False)
|
config_use_goodreads = Column(Boolean, default=False)
|
||||||
config_goodreads_api_key = Column(String)
|
config_goodreads_api_key = Column(String)
|
||||||
config_goodreads_api_secret = Column(String)
|
config_goodreads_api_secret = Column(String)
|
||||||
|
config_register_email = Column(Boolean, default=False)
|
||||||
config_login_type = Column(Integer, default=0)
|
config_login_type = Column(Integer, default=0)
|
||||||
|
|
||||||
# config_oauth_provider = Column(Integer)
|
config_kobo_proxy = Column(Boolean, default=False)
|
||||||
|
|
||||||
config_ldap_provider_url = Column(String, default='localhost')
|
config_ldap_provider_url = Column(String, default='example.org')
|
||||||
config_ldap_port = Column(SmallInteger, default=389)
|
config_ldap_port = Column(SmallInteger, default=389)
|
||||||
config_ldap_schema = Column(String, default='ldap')
|
config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE)
|
||||||
config_ldap_serv_username = Column(String)
|
config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
|
||||||
config_ldap_serv_password = Column(String)
|
config_ldap_serv_password = Column(String, default="")
|
||||||
config_ldap_use_ssl = Column(Boolean, default=False)
|
config_ldap_encryption = Column(SmallInteger, default=0)
|
||||||
config_ldap_use_tls = Column(Boolean, default=False)
|
config_ldap_cert_path = Column(String, default="")
|
||||||
config_ldap_require_cert = Column(Boolean, default=False)
|
config_ldap_dn = Column(String, default='dc=example,dc=org')
|
||||||
config_ldap_cert_path = Column(String)
|
config_ldap_user_object = Column(String, default='uid=%s')
|
||||||
config_ldap_dn = Column(String)
|
config_ldap_openldap = Column(Boolean, default=True)
|
||||||
config_ldap_user_object = Column(String)
|
config_ldap_group_object_filter = Column(String, default='(&(objectclass=posixGroup)(cn=%s))')
|
||||||
config_ldap_openldap = Column(Boolean, default=False)
|
config_ldap_group_members_field = Column(String, default='memberUid')
|
||||||
|
config_ldap_group_name = Column(String, default='calibreweb')
|
||||||
|
|
||||||
config_ebookconverter = Column(Integer, default=0)
|
config_kepubifypath = Column(String, default=None)
|
||||||
config_converterpath = Column(String)
|
config_converterpath = Column(String, default=None)
|
||||||
config_calibre = Column(String)
|
config_calibre = Column(String)
|
||||||
config_rarfile_location = Column(String)
|
config_rarfile_location = Column(String, default=None)
|
||||||
|
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
|
||||||
|
|
||||||
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
||||||
|
|
||||||
@@ -123,6 +142,22 @@ class _ConfigSQL(object):
|
|||||||
self.config_calibre_dir = None
|
self.config_calibre_dir = None
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
change = False
|
||||||
|
if self.config_converterpath == None:
|
||||||
|
change = True
|
||||||
|
self.config_converterpath = autodetect_calibre_binary()
|
||||||
|
|
||||||
|
if self.config_kepubifypath == None:
|
||||||
|
change = True
|
||||||
|
self.config_kepubifypath = autodetect_kepubify_binary()
|
||||||
|
|
||||||
|
if self.config_rarfile_location == None:
|
||||||
|
change = True
|
||||||
|
self.config_rarfile_location = autodetect_unrar_binary()
|
||||||
|
if change:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
def _read_from_storage(self):
|
def _read_from_storage(self):
|
||||||
if self._settings is None:
|
if self._settings is None:
|
||||||
log.debug("_ConfigSQL._read_from_storage")
|
log.debug("_ConfigSQL._read_from_storage")
|
||||||
@@ -179,12 +214,21 @@ class _ConfigSQL(object):
|
|||||||
def show_detail_random(self):
|
def show_detail_random(self):
|
||||||
return self.show_element_new_user(constants.DETAIL_RANDOM)
|
return self.show_element_new_user(constants.DETAIL_RANDOM)
|
||||||
|
|
||||||
def show_mature_content(self):
|
def list_denied_tags(self):
|
||||||
return self.show_element_new_user(constants.MATURE_CONTENT)
|
mct = self.config_denied_tags or ""
|
||||||
|
return [t.strip() for t in mct.split(",")]
|
||||||
|
|
||||||
def mature_content_tags(self):
|
def list_allowed_tags(self):
|
||||||
mct = self.config_mature_content_tags.split(",")
|
mct = self.config_allowed_tags or ""
|
||||||
return [t.strip() for t in mct]
|
return [t.strip() for t in mct.split(",")]
|
||||||
|
|
||||||
|
def list_denied_column_values(self):
|
||||||
|
mct = self.config_denied_column_value or ""
|
||||||
|
return [t.strip() for t in mct.split(",")]
|
||||||
|
|
||||||
|
def list_allowed_column_values(self):
|
||||||
|
mct = self.config_allowed_column_value or ""
|
||||||
|
return [t.strip() for t in mct.split(",")]
|
||||||
|
|
||||||
def get_log_level(self):
|
def get_log_level(self):
|
||||||
return logger.get_level_name(self.config_log_level)
|
return logger.get_level_name(self.config_log_level)
|
||||||
@@ -196,7 +240,7 @@ class _ConfigSQL(object):
|
|||||||
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER)
|
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER)
|
||||||
|
|
||||||
|
|
||||||
def set_from_dictionary(self, dictionary, field, convertor=None, default=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.
|
||||||
|
|
||||||
@@ -212,6 +256,9 @@ class _ConfigSQL(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if convertor is not None:
|
if convertor is not None:
|
||||||
|
if encode:
|
||||||
|
new_value = convertor(new_value.encode(encode))
|
||||||
|
else:
|
||||||
new_value = convertor(new_value)
|
new_value = convertor(new_value)
|
||||||
|
|
||||||
current_value = self.__dict__.get(field)
|
current_value = self.__dict__.get(field)
|
||||||
@@ -234,17 +281,19 @@ class _ConfigSQL(object):
|
|||||||
v = column.default.arg
|
v = column.default.arg
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
|
|
||||||
if self.config_google_drive_watch_changes_response:
|
|
||||||
self.config_google_drive_watch_changes_response = json.loads(self.config_google_drive_watch_changes_response)
|
|
||||||
|
|
||||||
have_metadata_db = bool(self.config_calibre_dir)
|
have_metadata_db = bool(self.config_calibre_dir)
|
||||||
if have_metadata_db:
|
if have_metadata_db:
|
||||||
if not self.config_use_google_drive:
|
if not self.config_use_google_drive:
|
||||||
db_file = os.path.join(self.config_calibre_dir, 'metadata.db')
|
db_file = os.path.join(self.config_calibre_dir, 'metadata.db')
|
||||||
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() for x in self.config_upload_formats.split(',')]
|
||||||
logger.setup(self.config_logfile, self.config_log_level)
|
logfile = logger.setup(self.config_logfile, self.config_log_level)
|
||||||
|
if logfile != self.config_logfile:
|
||||||
|
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
|
||||||
|
self.config_logfile = logfile
|
||||||
|
self._session.merge(s)
|
||||||
|
self._session.commit()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
'''Apply all configuration values to the underlying storage.'''
|
'''Apply all configuration values to the underlying storage.'''
|
||||||
@@ -261,7 +310,9 @@ class _ConfigSQL(object):
|
|||||||
self._session.commit()
|
self._session.commit()
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
def invalidate(self):
|
def invalidate(self, error=None):
|
||||||
|
if 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
|
||||||
@@ -279,7 +330,7 @@ def _migrate_table(session, orm_class):
|
|||||||
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 column.default is not None:
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
if isinstance(column.default.arg,unicode):
|
if isinstance(column.default.arg, unicode):
|
||||||
column.default.arg = column.default.arg.encode('utf-8')
|
column.default.arg = column.default.arg.encode('utf-8')
|
||||||
if column.default is None:
|
if column.default is None:
|
||||||
column_default = ""
|
column_default = ""
|
||||||
@@ -299,22 +350,47 @@ def _migrate_table(session, orm_class):
|
|||||||
if changed:
|
if changed:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
def autodetect_calibre_binary():
|
def autodetect_calibre_binary():
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
calibre_path = ["C:\\program files\calibre\calibre-convert.exe",
|
calibre_path = ["C:\\program files\\calibre\\ebook-convert.exe",
|
||||||
"C:\\program files(x86)\calibre\calibre-convert.exe"]
|
"C:\\program files(x86)\\calibre\\ebook-convert.exe",
|
||||||
|
"C:\\program files(x86)\\calibre2\\ebook-convert.exe",
|
||||||
|
"C:\\program files\\calibre2\\ebook-convert.exe"]
|
||||||
else:
|
else:
|
||||||
calibre_path = ["/opt/calibre/ebook-convert"]
|
calibre_path = ["/opt/calibre/ebook-convert"]
|
||||||
for element in calibre_path:
|
for element in calibre_path:
|
||||||
if os.path.isfile(element) and os.access(element, os.X_OK):
|
if os.path.isfile(element) and os.access(element, os.X_OK):
|
||||||
return element
|
return element
|
||||||
return None
|
return ""
|
||||||
|
|
||||||
|
def autodetect_unrar_binary():
|
||||||
|
if sys.platform == "win32":
|
||||||
|
calibre_path = ["C:\\program files\\WinRar\\unRAR.exe",
|
||||||
|
"C:\\program files(x86)\\WinRar\\unRAR.exe"]
|
||||||
|
else:
|
||||||
|
calibre_path = ["/usr/bin/unrar"]
|
||||||
|
for element in calibre_path:
|
||||||
|
if os.path.isfile(element) and os.access(element, os.X_OK):
|
||||||
|
return element
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def autodetect_kepubify_binary():
|
||||||
|
if sys.platform == "win32":
|
||||||
|
calibre_path = ["C:\\program files\\kepubify\\kepubify-windows-64Bit.exe",
|
||||||
|
"C:\\program files(x86)\\kepubify\\kepubify-windows-64Bit.exe"]
|
||||||
|
else:
|
||||||
|
calibre_path = ["/opt/kepubify/kepubify-linux-64bit", "/opt/kepubify/kepubify-linux-32bit"]
|
||||||
|
for element in calibre_path:
|
||||||
|
if os.path.isfile(element) and os.access(element, os.X_OK):
|
||||||
|
return element
|
||||||
|
return ""
|
||||||
|
|
||||||
def _migrate_database(session):
|
def _migrate_database(session):
|
||||||
# make sure the table is created, if it does not exist
|
# make sure the table is created, if it does not exist
|
||||||
_Base.metadata.create_all(session.bind)
|
_Base.metadata.create_all(session.bind)
|
||||||
_migrate_table(session, _Settings)
|
_migrate_table(session, _Settings)
|
||||||
|
_migrate_table(session, _Flask_Settings)
|
||||||
|
|
||||||
|
|
||||||
def load_configuration(session):
|
def load_configuration(session):
|
||||||
@@ -323,5 +399,20 @@ def load_configuration(session):
|
|||||||
if not session.query(_Settings).count():
|
if not session.query(_Settings).count():
|
||||||
session.add(_Settings())
|
session.add(_Settings())
|
||||||
session.commit()
|
session.commit()
|
||||||
|
conf = _ConfigSQL(session)
|
||||||
|
# Migrate from global restrictions to user based restrictions
|
||||||
|
if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
|
||||||
|
conf.config_denied_tags = conf.config_mature_content_tags
|
||||||
|
conf.save()
|
||||||
|
session.query(ub.User).filter(ub.User.mature_content != True). \
|
||||||
|
update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
|
||||||
|
session.commit()
|
||||||
|
return conf
|
||||||
|
|
||||||
return _ConfigSQL(session)
|
def get_flask_session_key(session):
|
||||||
|
flask_settings = session.query(_Flask_Settings).one_or_none()
|
||||||
|
if flask_settings == None:
|
||||||
|
flask_settings = _Flask_Settings(os.urandom(32))
|
||||||
|
session.add(flask_settings)
|
||||||
|
session.commit()
|
||||||
|
return flask_settings.flask_session_key
|
||||||
|
|||||||
+14
-12
@@ -80,9 +80,11 @@ MATURE_CONTENT = 1 << 11
|
|||||||
SIDEBAR_PUBLISHER = 1 << 12
|
SIDEBAR_PUBLISHER = 1 << 12
|
||||||
SIDEBAR_RATING = 1 << 13
|
SIDEBAR_RATING = 1 << 13
|
||||||
SIDEBAR_FORMAT = 1 << 14
|
SIDEBAR_FORMAT = 1 << 14
|
||||||
|
SIDEBAR_ARCHIVED = 1 << 15
|
||||||
|
# SIDEBAR_LIST = 1 << 16
|
||||||
|
|
||||||
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_EDIT_SHELFS & ~ROLE_ANONYMOUS
|
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
|
||||||
ADMIN_USER_SIDEBAR = (SIDEBAR_FORMAT << 1) - 1
|
ADMIN_USER_SIDEBAR = (SIDEBAR_ARCHIVED << 1) - 1
|
||||||
|
|
||||||
UPDATE_STABLE = 0 << 0
|
UPDATE_STABLE = 0 << 0
|
||||||
AUTO_UPDATE_STABLE = 1 << 0
|
AUTO_UPDATE_STABLE = 1 << 0
|
||||||
@@ -92,27 +94,27 @@ AUTO_UPDATE_NIGHTLY = 1 << 2
|
|||||||
LOGIN_STANDARD = 0
|
LOGIN_STANDARD = 0
|
||||||
LOGIN_LDAP = 1
|
LOGIN_LDAP = 1
|
||||||
LOGIN_OAUTH = 2
|
LOGIN_OAUTH = 2
|
||||||
# LOGIN_OAUTH_GOOGLE = 3
|
|
||||||
|
LDAP_AUTH_ANONYMOUS = 0
|
||||||
|
LDAP_AUTH_UNAUTHENTICATE = 1
|
||||||
|
LDAP_AUTH_SIMPLE = 0
|
||||||
|
|
||||||
DEFAULT_MAIL_SERVER = "mail.example.org"
|
DEFAULT_MAIL_SERVER = "mail.example.org"
|
||||||
|
|
||||||
DEFAULT_PASSWORD = "admin123"
|
DEFAULT_PASSWORD = "admin123"
|
||||||
DEFAULT_PORT = 8083
|
DEFAULT_PORT = 8083
|
||||||
|
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
|
||||||
try:
|
try:
|
||||||
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
|
|
||||||
DEFAULT_PORT = int(env_CALIBRE_PORT)
|
DEFAULT_PORT = int(env_CALIBRE_PORT)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print('Environment variable CALIBRE_PORT has invalid value (%s), faling back to default (8083)' % env_CALIBRE_PORT)
|
print('Environment variable CALIBRE_PORT has invalid value (%s), faling back to default (8083)' % env_CALIBRE_PORT)
|
||||||
del env_CALIBRE_PORT
|
del env_CALIBRE_PORT
|
||||||
|
|
||||||
|
|
||||||
|
EXTENSIONS_AUDIO = {'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'}
|
||||||
EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'}
|
EXTENSIONS_CONVERT = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt']
|
||||||
EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'}
|
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
|
||||||
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
|
'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'}
|
||||||
'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'}
|
|
||||||
# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] +
|
|
||||||
# (['rar','cbr'] if feature_support['rar'] else []))
|
|
||||||
|
|
||||||
|
|
||||||
def has_flag(value, bit_flag):
|
def has_flag(value, bit_flag):
|
||||||
@@ -126,7 +128,7 @@ def selected_roles(dictionary):
|
|||||||
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')
|
||||||
|
|
||||||
STABLE_VERSION = {'version': '0.6.5'}
|
STABLE_VERSION = {'version': '0.6.9'}
|
||||||
|
|
||||||
NIGHTLY_VERSION = {}
|
NIGHTLY_VERSION = {}
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
|
|||||||
+15
-9
@@ -19,6 +19,7 @@
|
|||||||
from __future__ import division, print_function, unicode_literals
|
from __future__ import division, print_function, unicode_literals
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from . import config, logger
|
from . import config, logger
|
||||||
@@ -29,8 +30,8 @@ log = logger.create()
|
|||||||
|
|
||||||
# _() necessary to make babel aware of string for translation
|
# _() necessary to make babel aware of string for translation
|
||||||
_NOT_CONFIGURED = _('not configured')
|
_NOT_CONFIGURED = _('not configured')
|
||||||
_NOT_INSTALLED = 'not installed'
|
_NOT_INSTALLED = _('not installed')
|
||||||
_EXECUTION_ERROR = 'Execution permissions missing'
|
_EXECUTION_ERROR = _('Execution permissions missing')
|
||||||
|
|
||||||
|
|
||||||
def _get_command_version(path, pattern, argument=None):
|
def _get_command_version(path, pattern, argument=None):
|
||||||
@@ -48,10 +49,15 @@ def _get_command_version(path, pattern, argument=None):
|
|||||||
return _NOT_INSTALLED
|
return _NOT_INSTALLED
|
||||||
|
|
||||||
|
|
||||||
def get_version():
|
def get_calibre_version():
|
||||||
version = None
|
return _get_command_version(config.config_converterpath, r'ebook-convert.*\(calibre', '--version') \
|
||||||
if config.config_ebookconverter == 1:
|
or _NOT_CONFIGURED
|
||||||
version = _get_command_version(config.config_converterpath, r'Amazon kindlegen\(')
|
|
||||||
elif config.config_ebookconverter == 2:
|
|
||||||
version = _get_command_version(config.config_converterpath, r'ebook-convert.*\(calibre', '--version')
|
def get_unrar_version():
|
||||||
return version or _NOT_CONFIGURED
|
return _get_command_version(config.config_rarfile_location, r'UNRAR.*\d') or _NOT_CONFIGURED
|
||||||
|
|
||||||
|
def get_kepubify_version():
|
||||||
|
return _get_command_version(config.config_kepubifypath, r'kepubify\s','--version') or _NOT_CONFIGURED
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,18 +22,36 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import ast
|
import ast
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import threading
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy import Table, Column, ForeignKey
|
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
||||||
from sqlalchemy import String, Integer, Boolean
|
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.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
from flask_login import current_user
|
||||||
|
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from babel import Locale as LC
|
||||||
|
from babel.core import UnknownLocaleError
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
from . import logger, ub, isoLanguages
|
||||||
|
from .pagination import Pagination
|
||||||
|
|
||||||
|
try:
|
||||||
|
import unidecode
|
||||||
|
use_unidecode = True
|
||||||
|
except ImportError:
|
||||||
|
use_unidecode = False
|
||||||
|
|
||||||
|
|
||||||
session = None
|
cc_exceptions = ['datetime', 'comments', 'composite', 'series']
|
||||||
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
|
|
||||||
cc_classes = {}
|
cc_classes = {}
|
||||||
engine = None
|
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
@@ -72,9 +90,9 @@ class Identifiers(Base):
|
|||||||
__tablename__ = 'identifiers'
|
__tablename__ = 'identifiers'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
type = Column(String)
|
type = Column(String(collation='NOCASE'), nullable=False, default="isbn")
|
||||||
val = Column(String)
|
val = Column(String(collation='NOCASE'), nullable=False)
|
||||||
book = Column(Integer, ForeignKey('books.id'))
|
book = Column(Integer, ForeignKey('books.id'), nullable=False)
|
||||||
|
|
||||||
def __init__(self, val, id_type, book):
|
def __init__(self, val, id_type, book):
|
||||||
self.val = val
|
self.val = val
|
||||||
@@ -82,41 +100,61 @@ class Identifiers(Base):
|
|||||||
self.book = book
|
self.book = book
|
||||||
|
|
||||||
def formatType(self):
|
def formatType(self):
|
||||||
if self.type == "amazon":
|
format_type = self.type.lower()
|
||||||
|
if format_type == 'amazon':
|
||||||
return u"Amazon"
|
return u"Amazon"
|
||||||
elif self.type == "isbn":
|
elif format_type.startswith("amazon_"):
|
||||||
|
return u"Amazon.{0}".format(format_type[7:])
|
||||||
|
elif format_type == "isbn":
|
||||||
return u"ISBN"
|
return u"ISBN"
|
||||||
elif self.type == "doi":
|
elif format_type == "doi":
|
||||||
return u"DOI"
|
return u"DOI"
|
||||||
elif self.type == "goodreads":
|
elif format_type == "douban":
|
||||||
|
return u"Douban"
|
||||||
|
elif format_type == "goodreads":
|
||||||
return u"Goodreads"
|
return u"Goodreads"
|
||||||
elif self.type == "google":
|
elif format_type == "google":
|
||||||
return u"Google Books"
|
return u"Google Books"
|
||||||
elif self.type == "kobo":
|
elif format_type == "kobo":
|
||||||
return u"Kobo"
|
return u"Kobo"
|
||||||
if self.type == "lubimyczytac":
|
elif format_type == "litres":
|
||||||
|
return u"ЛитРес"
|
||||||
|
elif format_type == "issn":
|
||||||
|
return u"ISSN"
|
||||||
|
elif format_type == "isfdb":
|
||||||
|
return u"ISFDB"
|
||||||
|
if format_type == "lubimyczytac":
|
||||||
return u"Lubimyczytac"
|
return u"Lubimyczytac"
|
||||||
else:
|
else:
|
||||||
return self.type
|
return self.type
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.type == "amazon":
|
format_type = self.type.lower()
|
||||||
return u"https://amzn.com/{0}".format(self.val)
|
if format_type == "amazon" or format_type == "asin":
|
||||||
elif self.type == "isbn":
|
return u"https://amazon.com/dp/{0}".format(self.val)
|
||||||
return u"http://www.worldcat.org/isbn/{0}".format(self.val)
|
elif format_type.startswith('amazon_'):
|
||||||
elif self.type == "doi":
|
return u"https://amazon.{0}/dp/{1}".format(format_type[7:], self.val)
|
||||||
return u"http://dx.doi.org/{0}".format(self.val)
|
elif format_type == "isbn":
|
||||||
elif self.type == "goodreads":
|
return u"https://www.worldcat.org/isbn/{0}".format(self.val)
|
||||||
return u"http://www.goodreads.com/book/show/{0}".format(self.val)
|
elif format_type == "doi":
|
||||||
elif self.type == "douban":
|
return u"https://dx.doi.org/{0}".format(self.val)
|
||||||
|
elif format_type == "goodreads":
|
||||||
|
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
|
||||||
|
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 self.type == "google":
|
elif format_type == "google":
|
||||||
return u"https://books.google.com/books?id={0}".format(self.val)
|
return u"https://books.google.com/books?id={0}".format(self.val)
|
||||||
elif self.type == "kobo":
|
elif format_type == "kobo":
|
||||||
return u"https://www.kobo.com/ebook/{0}".format(self.val)
|
return u"https://www.kobo.com/ebook/{0}".format(self.val)
|
||||||
elif self.type == "lubimyczytac":
|
elif format_type == "lubimyczytac":
|
||||||
return u" http://lubimyczytac.pl/ksiazka/{0}".format(self.val)
|
return u" https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val)
|
||||||
elif self.type == "url":
|
elif format_type == "litres":
|
||||||
|
return u"https://www.litres.ru/{0}".format(self.val)
|
||||||
|
elif format_type == "issn":
|
||||||
|
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
||||||
|
elif format_type == "isfdb":
|
||||||
|
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
||||||
|
elif format_type == "url":
|
||||||
return u"{0}".format(self.val)
|
return u"{0}".format(self.val)
|
||||||
else:
|
else:
|
||||||
return u""
|
return u""
|
||||||
@@ -126,8 +164,8 @@ class Comments(Base):
|
|||||||
__tablename__ = 'comments'
|
__tablename__ = 'comments'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
text = Column(String)
|
text = Column(String(collation='NOCASE'), nullable=False)
|
||||||
book = Column(Integer, ForeignKey('books.id'))
|
book = Column(Integer, ForeignKey('books.id'), nullable=False)
|
||||||
|
|
||||||
def __init__(self, text, book):
|
def __init__(self, text, book):
|
||||||
self.text = text
|
self.text = text
|
||||||
@@ -141,7 +179,7 @@ class Tags(Base):
|
|||||||
__tablename__ = 'tags'
|
__tablename__ = 'tags'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
name = Column(String)
|
name = Column(String(collation='NOCASE'), unique=True, nullable=False)
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -154,9 +192,9 @@ class Authors(Base):
|
|||||||
__tablename__ = 'authors'
|
__tablename__ = 'authors'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String)
|
name = Column(String(collation='NOCASE'), unique=True, nullable=False)
|
||||||
sort = Column(String)
|
sort = Column(String(collation='NOCASE'))
|
||||||
link = Column(String)
|
link = Column(String, nullable=False, default="")
|
||||||
|
|
||||||
def __init__(self, name, sort, link):
|
def __init__(self, name, sort, link):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -171,8 +209,8 @@ class Series(Base):
|
|||||||
__tablename__ = 'series'
|
__tablename__ = 'series'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String)
|
name = Column(String(collation='NOCASE'), unique=True, nullable=False)
|
||||||
sort = Column(String)
|
sort = Column(String(collation='NOCASE'))
|
||||||
|
|
||||||
def __init__(self, name, sort):
|
def __init__(self, name, sort):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -186,7 +224,7 @@ class Ratings(Base):
|
|||||||
__tablename__ = 'ratings'
|
__tablename__ = 'ratings'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
rating = Column(Integer)
|
rating = Column(Integer, CheckConstraint('rating>-1 AND rating<11'), unique=True)
|
||||||
|
|
||||||
def __init__(self, rating):
|
def __init__(self, rating):
|
||||||
self.rating = rating
|
self.rating = rating
|
||||||
@@ -199,7 +237,7 @@ class Languages(Base):
|
|||||||
__tablename__ = 'languages'
|
__tablename__ = 'languages'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
lang_code = Column(String)
|
lang_code = Column(String(collation='NOCASE'), nullable=False, unique=True)
|
||||||
|
|
||||||
def __init__(self, lang_code):
|
def __init__(self, lang_code):
|
||||||
self.lang_code = lang_code
|
self.lang_code = lang_code
|
||||||
@@ -212,8 +250,8 @@ class Publishers(Base):
|
|||||||
__tablename__ = 'publishers'
|
__tablename__ = 'publishers'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String)
|
name = Column(String(collation='NOCASE'), nullable=False, unique=True)
|
||||||
sort = Column(String)
|
sort = Column(String(collation='NOCASE'))
|
||||||
|
|
||||||
def __init__(self, name, sort):
|
def __init__(self, name, sort):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -225,12 +263,13 @@ class Publishers(Base):
|
|||||||
|
|
||||||
class Data(Base):
|
class Data(Base):
|
||||||
__tablename__ = 'data'
|
__tablename__ = 'data'
|
||||||
|
__table_args__ = {'schema':'calibre'}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
book = Column(Integer, ForeignKey('books.id'))
|
book = Column(Integer, ForeignKey('books.id'), nullable=False)
|
||||||
format = Column(String)
|
format = Column(String(collation='NOCASE'), nullable=False)
|
||||||
uncompressed_size = Column(Integer)
|
uncompressed_size = Column(Integer, nullable=False)
|
||||||
name = Column(String)
|
name = Column(String, nullable=False)
|
||||||
|
|
||||||
def __init__(self, book, book_format, uncompressed_size, name):
|
def __init__(self, book, book_format, uncompressed_size, name):
|
||||||
self.book = book
|
self.book = book
|
||||||
@@ -247,20 +286,23 @@ class Books(Base):
|
|||||||
|
|
||||||
DEFAULT_PUBDATE = "0101-01-01 00:00:00+00:00"
|
DEFAULT_PUBDATE = "0101-01-01 00:00:00+00:00"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
title = Column(String)
|
title = Column(String(collation='NOCASE'), nullable=False, default='Unknown')
|
||||||
sort = Column(String)
|
sort = Column(String(collation='NOCASE'))
|
||||||
author_sort = Column(String)
|
author_sort = Column(String(collation='NOCASE'))
|
||||||
timestamp = Column(String)
|
timestamp = Column(TIMESTAMP, default=datetime.utcnow)
|
||||||
pubdate = Column(String)
|
pubdate = Column(String) # , default=datetime.utcnow)
|
||||||
series_index = Column(String)
|
series_index = Column(String, nullable=False, default="1.0")
|
||||||
last_modified = Column(String)
|
last_modified = Column(TIMESTAMP, default=datetime.utcnow)
|
||||||
path = Column(String)
|
path = Column(String, default="", nullable=False)
|
||||||
has_cover = Column(Integer)
|
has_cover = Column(Integer, default=0)
|
||||||
uuid = Column(String)
|
uuid = Column(String)
|
||||||
|
isbn = Column(String(collation='NOCASE'), default="")
|
||||||
|
# Iccn = Column(String(collation='NOCASE'), default="")
|
||||||
|
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')
|
||||||
tags = relationship('Tags', secondary=books_tags_link, backref='books',order_by="Tags.name")
|
tags = relationship('Tags', secondary=books_tags_link, backref='books', order_by="Tags.name")
|
||||||
comments = relationship('Comments', backref='books')
|
comments = relationship('Comments', backref='books')
|
||||||
data = relationship('Data', backref='books')
|
data = relationship('Data', backref='books')
|
||||||
series = relationship('Series', secondary=books_series_link, backref='books')
|
series = relationship('Series', secondary=books_series_link, backref='books')
|
||||||
@@ -310,24 +352,46 @@ class Custom_Columns(Base):
|
|||||||
return display_dict
|
return display_dict
|
||||||
|
|
||||||
|
|
||||||
def update_title_sort(config, conn=None):
|
class CalibreDB(threading.Thread):
|
||||||
# user defined sort function for calibre databases (Series, etc.)
|
|
||||||
def _title_sort(title):
|
|
||||||
# calibre sort stuff
|
|
||||||
title_pat = re.compile(config.config_title_regex, re.IGNORECASE)
|
|
||||||
match = title_pat.search(title)
|
|
||||||
if match:
|
|
||||||
prep = match.group(1)
|
|
||||||
title = title.replace(prep, '') + ', ' + prep
|
|
||||||
return title.strip()
|
|
||||||
|
|
||||||
conn = conn or session.connection().connection.connection
|
def __init__(self):
|
||||||
conn.create_function("title_sort", 1, _title_sort)
|
threading.Thread.__init__(self)
|
||||||
|
self.engine = None
|
||||||
|
self.session = None
|
||||||
|
self.queue = None
|
||||||
|
self.log = None
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
def add_queue(self,queue):
|
||||||
|
self.queue = queue
|
||||||
|
self.log = logger.create()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
i = self.queue.get()
|
||||||
|
if i == 'dummy':
|
||||||
|
self.queue.task_done()
|
||||||
|
break
|
||||||
|
if i['task'] == 'add_format':
|
||||||
|
cur_book = self.session.query(Books).filter(Books.id == i['id']).first()
|
||||||
|
cur_book.data.append(i['format'])
|
||||||
|
try:
|
||||||
|
# db.session.merge(cur_book)
|
||||||
|
self.session.commit()
|
||||||
|
except OperationalError as e:
|
||||||
|
self.session.rollback()
|
||||||
|
self.log.error("Database error: %s", e)
|
||||||
|
# self._handleError(_(u"Database error: %(error)s.", error=e))
|
||||||
|
# return
|
||||||
|
self.queue.task_done()
|
||||||
|
|
||||||
|
|
||||||
def setup_db(config):
|
def stop(self):
|
||||||
dispose()
|
self.queue.put('dummy')
|
||||||
global engine
|
|
||||||
|
def setup_db(self, config, app_db_path):
|
||||||
|
self.config = config
|
||||||
|
self.dispose()
|
||||||
|
|
||||||
if not config.config_calibre_dir:
|
if not config.config_calibre_dir:
|
||||||
config.invalidate()
|
config.invalidate()
|
||||||
@@ -339,19 +403,22 @@ def setup_db(config):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
engine = create_engine('sqlite:///{0}'.format(dbpath),
|
self.engine = create_engine('sqlite://',
|
||||||
echo=False,
|
echo=False,
|
||||||
isolation_level="SERIALIZABLE",
|
isolation_level="SERIALIZABLE",
|
||||||
connect_args={'check_same_thread': False})
|
connect_args={'check_same_thread': False},
|
||||||
conn = engine.connect()
|
poolclass=StaticPool)
|
||||||
except:
|
self.engine.execute("attach database '{}' as calibre;".format(dbpath))
|
||||||
config.invalidate()
|
self.engine.execute("attach database '{}' as app_settings;".format(app_db_path))
|
||||||
|
|
||||||
|
conn = self.engine.connect()
|
||||||
|
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
||||||
|
except Exception as e:
|
||||||
|
config.invalidate(e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
config.db_configured = True
|
config.db_configured = True
|
||||||
update_title_sort(config, conn.connection)
|
self.update_title_sort(config, conn.connection)
|
||||||
# conn.connection.create_function('lower', 1, lcase)
|
|
||||||
# conn.connection.create_function('upper', 1, ucase)
|
|
||||||
|
|
||||||
if not cc_classes:
|
if not cc_classes:
|
||||||
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
||||||
@@ -360,61 +427,240 @@ def setup_db(config):
|
|||||||
books_custom_column_links = {}
|
books_custom_column_links = {}
|
||||||
for row in cc:
|
for row in cc:
|
||||||
if row.datatype not in cc_exceptions:
|
if row.datatype not in cc_exceptions:
|
||||||
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata,
|
if row.datatype == 'series':
|
||||||
|
dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link',
|
||||||
|
'id': Column(Integer, primary_key=True),
|
||||||
|
'book': Column(Integer, ForeignKey('books.id'),
|
||||||
|
primary_key=True),
|
||||||
|
'map_value': Column('value', Integer,
|
||||||
|
ForeignKey('custom_column_' +
|
||||||
|
str(row.id) + '.id'),
|
||||||
|
primary_key=True),
|
||||||
|
'extra': Column(Float),
|
||||||
|
'asoc' : relationship('custom_column_' + str(row.id), uselist=False),
|
||||||
|
'value' : association_proxy('asoc', 'value')
|
||||||
|
}
|
||||||
|
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
|
||||||
|
(Base,), dicttable)
|
||||||
|
else:
|
||||||
|
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
|
||||||
|
Base.metadata,
|
||||||
Column('book', Integer, ForeignKey('books.id'),
|
Column('book', Integer, ForeignKey('books.id'),
|
||||||
primary_key=True),
|
primary_key=True),
|
||||||
Column('value', Integer,
|
Column('value', Integer,
|
||||||
ForeignKey('custom_column_' + str(row.id) + '.id'),
|
ForeignKey('custom_column_' +
|
||||||
|
str(row.id) + '.id'),
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
)
|
)
|
||||||
cc_ids.append([row.id, row.datatype])
|
cc_ids.append([row.id, row.datatype])
|
||||||
if row.datatype == 'bool':
|
|
||||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
||||||
'id': Column(Integer, primary_key=True),
|
'id': Column(Integer, primary_key=True)}
|
||||||
'book': Column(Integer, ForeignKey('books.id')),
|
if row.datatype == 'float':
|
||||||
'value': Column(Boolean)}
|
ccdict['value'] = Column(Float)
|
||||||
elif row.datatype == 'int':
|
elif row.datatype == 'int':
|
||||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
ccdict['value'] = Column(Integer)
|
||||||
'id': Column(Integer, primary_key=True),
|
elif row.datatype == 'bool':
|
||||||
'book': Column(Integer, ForeignKey('books.id')),
|
ccdict['value'] = Column(Boolean)
|
||||||
'value': Column(Integer)}
|
|
||||||
else:
|
else:
|
||||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
ccdict['value'] = Column(String)
|
||||||
'id': Column(Integer, primary_key=True),
|
if row.datatype in ['float', 'int', 'bool']:
|
||||||
'value': Column(String)}
|
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'):
|
if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'):
|
||||||
setattr(Books, 'custom_column_' + str(cc_id[0]), relationship(cc_classes[cc_id[0]],
|
setattr(Books,
|
||||||
|
'custom_column_' + str(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'):
|
||||||
|
setattr(Books,
|
||||||
|
'custom_column_' + str(cc_id[0]),
|
||||||
|
relationship(books_custom_column_links[cc_id[0]],
|
||||||
|
backref='books'))
|
||||||
else:
|
else:
|
||||||
setattr(Books, 'custom_column_' + str(cc_id[0]), relationship(cc_classes[cc_id[0]],
|
setattr(Books,
|
||||||
|
'custom_column_' + str(cc_id[0]),
|
||||||
|
relationship(cc_classes[cc_id[0]],
|
||||||
secondary=books_custom_column_links[cc_id[0]],
|
secondary=books_custom_column_links[cc_id[0]],
|
||||||
backref='books'))
|
backref='books'))
|
||||||
|
|
||||||
|
|
||||||
global session
|
|
||||||
Session = scoped_session(sessionmaker(autocommit=False,
|
Session = scoped_session(sessionmaker(autocommit=False,
|
||||||
autoflush=False,
|
autoflush=False,
|
||||||
bind=engine))
|
bind=self.engine))
|
||||||
session = Session()
|
self.session = Session()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_book(self, book_id):
|
||||||
|
return self.session.query(Books).filter(Books.id == book_id).first()
|
||||||
|
|
||||||
def dispose():
|
def get_filtered_book(self, book_id, allow_show_archived=False):
|
||||||
global session
|
return self.session.query(Books).filter(Books.id == book_id).\
|
||||||
|
filter(self.common_filters(allow_show_archived)).first()
|
||||||
|
|
||||||
old_session = session
|
def get_book_by_uuid(self, book_uuid):
|
||||||
session = None
|
return self.session.query(Books).filter(Books.uuid == book_uuid).first()
|
||||||
|
|
||||||
|
def get_book_format(self, book_id, format):
|
||||||
|
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == format).first()
|
||||||
|
|
||||||
|
# Language and content filters for displaying in the UI
|
||||||
|
def common_filters(self, allow_show_archived=False):
|
||||||
|
if not allow_show_archived:
|
||||||
|
archived_books = (
|
||||||
|
ub.session.query(ub.ArchivedBook)
|
||||||
|
.filter(ub.ArchivedBook.user_id == int(current_user.id))
|
||||||
|
.filter(ub.ArchivedBook.is_archived == True)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
archived_book_ids = [archived_book.book_id for archived_book in archived_books]
|
||||||
|
archived_filter = Books.id.notin_(archived_book_ids)
|
||||||
|
else:
|
||||||
|
archived_filter = true()
|
||||||
|
|
||||||
|
if current_user.filter_language() != "all":
|
||||||
|
lang_filter = Books.languages.any(Languages.lang_code == current_user.filter_language())
|
||||||
|
else:
|
||||||
|
lang_filter = true()
|
||||||
|
negtags_list = current_user.list_denied_tags()
|
||||||
|
postags_list = current_user.list_allowed_tags()
|
||||||
|
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))
|
||||||
|
if self.config.config_restricted_column:
|
||||||
|
pos_cc_list = current_user.allowed_column_value.split(',')
|
||||||
|
pos_content_cc_filter = true() if pos_cc_list == [''] else \
|
||||||
|
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
||||||
|
any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list))
|
||||||
|
neg_cc_list = current_user.denied_column_value.split(',')
|
||||||
|
neg_content_cc_filter = false() if neg_cc_list == [''] else \
|
||||||
|
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
||||||
|
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
|
||||||
|
else:
|
||||||
|
pos_content_cc_filter = true()
|
||||||
|
neg_content_cc_filter = false()
|
||||||
|
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
|
||||||
|
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
|
||||||
|
|
||||||
|
# Fill indexpage with all requested data from database
|
||||||
|
def fill_indexpage(self, page, database, db_filter, order, *join):
|
||||||
|
return self.fill_indexpage_with_archived_books(page, database, db_filter, order, False, *join)
|
||||||
|
|
||||||
|
def fill_indexpage_with_archived_books(self, page, database, db_filter, order, allow_show_archived, *join):
|
||||||
|
if current_user.show_detail_random():
|
||||||
|
randm = self.session.query(Books) \
|
||||||
|
.filter(self.common_filters(allow_show_archived)) \
|
||||||
|
.order_by(func.random()) \
|
||||||
|
.limit(self.config.config_random_books)
|
||||||
|
else:
|
||||||
|
randm = false()
|
||||||
|
off = int(int(self.config.config_books_per_page) * (page - 1))
|
||||||
|
query = self.session.query(database) \
|
||||||
|
.join(*join, isouter=True) \
|
||||||
|
.filter(db_filter) \
|
||||||
|
.filter(self.common_filters(allow_show_archived))
|
||||||
|
pagination = Pagination(page, self.config.config_books_per_page,
|
||||||
|
len(query.all()))
|
||||||
|
entries = query.order_by(*order).offset(off).limit(self.config.config_books_per_page).all()
|
||||||
|
for book in entries:
|
||||||
|
book = self.order_authors(book)
|
||||||
|
return entries, randm, pagination
|
||||||
|
|
||||||
|
# Orders all Authors in the list according to authors sort
|
||||||
|
def order_authors(self, entry):
|
||||||
|
sort_authors = entry.author_sort.split('&')
|
||||||
|
authors_ordered = list()
|
||||||
|
error = False
|
||||||
|
for auth in sort_authors:
|
||||||
|
# ToDo: How to handle not found authorname
|
||||||
|
result = self.session.query(Authors).filter(Authors.sort == auth.lstrip().strip()).first()
|
||||||
|
if not result:
|
||||||
|
error = True
|
||||||
|
break
|
||||||
|
authors_ordered.append(result)
|
||||||
|
if not error:
|
||||||
|
entry.authors = authors_ordered
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()):
|
||||||
|
query = query or ''
|
||||||
|
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
|
entries = self.session.query(database).filter(tag_filter). \
|
||||||
|
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
||||||
|
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
||||||
|
return json_dumps
|
||||||
|
|
||||||
|
def check_exists_book(self, authr, title):
|
||||||
|
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
|
q = list()
|
||||||
|
authorterms = re.split(r'\s*&\s*', authr)
|
||||||
|
for authorterm in authorterms:
|
||||||
|
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
|
||||||
|
|
||||||
|
return self.session.query(Books)\
|
||||||
|
.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 get_search_results(self, term):
|
||||||
|
term.strip().lower()
|
||||||
|
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
|
q = list()
|
||||||
|
authorterms = re.split("[, ]+", term)
|
||||||
|
for authorterm in authorterms:
|
||||||
|
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
|
||||||
|
return self.session.query(Books).filter(self.common_filters(True)).filter(
|
||||||
|
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
|
||||||
|
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
|
||||||
|
Books.authors.any(and_(*q)),
|
||||||
|
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
|
||||||
|
func.lower(Books.title).ilike("%" + term + "%")
|
||||||
|
)).order_by(Books.sort).all()
|
||||||
|
|
||||||
|
# Creates for all stored languages a translated speaking name in the array for the UI
|
||||||
|
def speaking_language(self, languages=None):
|
||||||
|
from . import get_locale
|
||||||
|
|
||||||
|
if not languages:
|
||||||
|
languages = self.session.query(Languages) \
|
||||||
|
.join(books_languages_link) \
|
||||||
|
.join(Books) \
|
||||||
|
.filter(self.common_filters()) \
|
||||||
|
.group_by(text('books_languages_link.lang_code')).all()
|
||||||
|
for lang in languages:
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
|
||||||
|
def update_title_sort(self, config, conn=None):
|
||||||
|
# user defined sort function for calibre databases (Series, etc.)
|
||||||
|
def _title_sort(title):
|
||||||
|
# calibre sort stuff
|
||||||
|
title_pat = re.compile(config.config_title_regex, re.IGNORECASE)
|
||||||
|
match = title_pat.search(title)
|
||||||
|
if match:
|
||||||
|
prep = match.group(1)
|
||||||
|
title = title[len(prep):] + ', ' + prep
|
||||||
|
return title.strip()
|
||||||
|
|
||||||
|
conn = conn or self.session.connection().connection.connection
|
||||||
|
conn.create_function("title_sort", 1, _title_sort)
|
||||||
|
|
||||||
|
def dispose(self):
|
||||||
|
# global session
|
||||||
|
|
||||||
|
old_session = self.session
|
||||||
|
self.session = None
|
||||||
if old_session:
|
if old_session:
|
||||||
try: old_session.close()
|
try: old_session.close()
|
||||||
except: pass
|
except: pass
|
||||||
if old_session.bind:
|
if old_session.bind:
|
||||||
try: old_session.bind.dispose()
|
try: old_session.bind.dispose()
|
||||||
except: pass
|
except Exception: pass
|
||||||
|
|
||||||
for attr in list(Books.__dict__.keys()):
|
for attr in list(Books.__dict__.keys()):
|
||||||
if attr.startswith("custom_column_"):
|
if attr.startswith("custom_column_"):
|
||||||
@@ -430,7 +676,15 @@ def dispose():
|
|||||||
if table is not None:
|
if table is not None:
|
||||||
Base.metadata.remove(table)
|
Base.metadata.remove(table)
|
||||||
|
|
||||||
def reconnect_db(config):
|
def reconnect_db(self, config, app_db_path):
|
||||||
session.close()
|
self.session.close()
|
||||||
engine.dispose()
|
self.engine.dispose()
|
||||||
setup_db(config)
|
self.setup_db(config, app_db_path)
|
||||||
|
|
||||||
|
def lcase(s):
|
||||||
|
try:
|
||||||
|
return unidecode.unidecode(s.lower())
|
||||||
|
except Exception as e:
|
||||||
|
log = logger.create()
|
||||||
|
log.exception(e)
|
||||||
|
return s.lower()
|
||||||
|
|||||||
+375
-215
@@ -22,18 +22,19 @@
|
|||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
from __future__ import division, print_function, unicode_literals
|
||||||
import os
|
import os
|
||||||
import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
from shutil import move, copyfile
|
from shutil import copyfile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper
|
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper
|
||||||
from . import config, get_locale, db, ub, worker
|
from . import config, get_locale, ub, worker, db
|
||||||
from .helper import order_authors, common_filters
|
from . import calibre_db
|
||||||
from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required
|
from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
# passing input_elements not as a list may lead to undesired results
|
# passing input_elements not as a list may lead to undesired results
|
||||||
if not isinstance(input_elements, list):
|
if not isinstance(input_elements, list):
|
||||||
raise TypeError(str(input_elements) + " should be passed as a list")
|
raise TypeError(str(input_elements) + " should be passed as a list")
|
||||||
|
changed = False
|
||||||
input_elements = [x for x in input_elements if x != '']
|
input_elements = [x for x in input_elements if x != '']
|
||||||
# we have all input element (authors, series, tags) names now
|
# we have all input element (authors, series, tags) names now
|
||||||
# 1. search for elements to remove
|
# 1. search for elements to remove
|
||||||
@@ -88,6 +89,7 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
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
|
||||||
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!
|
# if there are elements to add, we add them now!
|
||||||
@@ -114,37 +116,61 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
else: # db_type should be tag or language
|
else: # db_type should be tag or language
|
||||||
new_element = db_object(add_element)
|
new_element = db_object(add_element)
|
||||||
if db_element is None:
|
if db_element is None:
|
||||||
|
changed = True
|
||||||
db_session.add(new_element)
|
db_session.add(new_element)
|
||||||
db_book_object.append(new_element)
|
db_book_object.append(new_element)
|
||||||
else:
|
else:
|
||||||
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
|
new_element.value = add_element
|
||||||
# new_element = db_element
|
|
||||||
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
|
||||||
# new_element = db_element
|
|
||||||
elif db_type == 'series':
|
elif db_type == 'series':
|
||||||
if db_element.name != add_element:
|
if db_element.name != add_element:
|
||||||
db_element.name = add_element # = add_element # new_element = db_object(add_element, add_element)
|
db_element.name = add_element
|
||||||
db_element.sort = add_element
|
db_element.sort = add_element
|
||||||
# new_element = db_element
|
|
||||||
elif db_type == 'author':
|
elif db_type == 'author':
|
||||||
if db_element.name != add_element:
|
if db_element.name != add_element:
|
||||||
db_element.name = add_element
|
db_element.name = add_element
|
||||||
db_element.sort = add_element.replace('|', ',')
|
db_element.sort = add_element.replace('|', ',')
|
||||||
# new_element = db_element
|
|
||||||
elif db_type == 'publisher':
|
elif db_type == 'publisher':
|
||||||
if db_element.name != add_element:
|
if db_element.name != add_element:
|
||||||
db_element.name = add_element
|
db_element.name = add_element
|
||||||
db_element.sort = None
|
db_element.sort = None
|
||||||
# new_element = db_element
|
|
||||||
elif db_element.name != add_element:
|
elif db_element.name != add_element:
|
||||||
db_element.name = add_element
|
db_element.name = add_element
|
||||||
# new_element = db_element
|
|
||||||
# add element to book
|
# add element to book
|
||||||
|
changed = True
|
||||||
db_book_object.append(db_element)
|
db_book_object.append(db_element)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def modify_identifiers(input_identifiers, db_identifiers, db_session):
|
||||||
|
"""Modify Identifiers to match input information.
|
||||||
|
input_identifiers is a list of read-to-persist Identifiers objects.
|
||||||
|
db_identifiers is a list of already persisted list of Identifiers objects."""
|
||||||
|
changed = False
|
||||||
|
error = False
|
||||||
|
input_dict = dict([(identifier.type.lower(), identifier) for identifier in input_identifiers])
|
||||||
|
if len(input_identifiers) != len(input_dict):
|
||||||
|
error = True
|
||||||
|
db_dict = dict([(identifier.type.lower(), identifier) for identifier in db_identifiers ])
|
||||||
|
# delete db identifiers not present in input or modify them with input val
|
||||||
|
for identifier_type, identifier in db_dict.items():
|
||||||
|
if identifier_type not in input_dict.keys():
|
||||||
|
db_session.delete(identifier)
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
input_identifier = input_dict[identifier_type]
|
||||||
|
identifier.type = input_identifier.type
|
||||||
|
identifier.val = input_identifier.val
|
||||||
|
# add input identifiers not present in db
|
||||||
|
for identifier_type, identifier in input_dict.items():
|
||||||
|
if identifier_type not in db_dict.keys():
|
||||||
|
db_session.add(identifier)
|
||||||
|
changed = True
|
||||||
|
return changed, error
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/delete/<int:book_id>/", defaults={'book_format': ""})
|
@editbook.route("/delete/<int:book_id>/", defaults={'book_format': ""})
|
||||||
@@ -152,9 +178,15 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
@login_required
|
@login_required
|
||||||
def delete_book(book_id, book_format):
|
def delete_book(book_id, book_format):
|
||||||
if current_user.role_delete_books():
|
if current_user.role_delete_books():
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
book = calibre_db.get_book(book_id)
|
||||||
if book:
|
if book:
|
||||||
helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
|
try:
|
||||||
|
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
|
||||||
|
if not result:
|
||||||
|
flash(error, category="error")
|
||||||
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
|
if error:
|
||||||
|
flash(error, category="warning")
|
||||||
if not book_format:
|
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()
|
||||||
@@ -164,52 +196,63 @@ def delete_book(book_id, book_format):
|
|||||||
|
|
||||||
# check if only this book links to:
|
# check if only this book links to:
|
||||||
# author, language, series, tags, custom columns
|
# author, language, series, tags, custom columns
|
||||||
modify_database_object([u''], book.authors, db.Authors, db.session, 'author')
|
modify_database_object([u''], book.authors, db.Authors, calibre_db.session, 'author')
|
||||||
modify_database_object([u''], book.tags, db.Tags, db.session, 'tags')
|
modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags')
|
||||||
modify_database_object([u''], book.series, db.Series, db.session, 'series')
|
modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series')
|
||||||
modify_database_object([u''], book.languages, db.Languages, db.session, 'languages')
|
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
|
||||||
modify_database_object([u''], book.publishers, db.Publishers, db.session, 'publishers')
|
modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers')
|
||||||
|
|
||||||
cc = 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()
|
||||||
for c in cc:
|
for c in cc:
|
||||||
cc_string = "custom_column_" + str(c.id)
|
cc_string = "custom_column_" + str(c.id)
|
||||||
if not c.is_multiple:
|
if not c.is_multiple:
|
||||||
if len(getattr(book, cc_string)) > 0:
|
if len(getattr(book, cc_string)) > 0:
|
||||||
if c.datatype == 'bool' or c.datatype == 'integer':
|
if c.datatype == 'bool' or c.datatype == 'integer' or c.datatype == 'float':
|
||||||
del_cc = getattr(book, cc_string)[0]
|
del_cc = getattr(book, cc_string)[0]
|
||||||
getattr(book, cc_string).remove(del_cc)
|
getattr(book, cc_string).remove(del_cc)
|
||||||
db.session.delete(del_cc)
|
log.debug('remove ' + str(c.id))
|
||||||
|
calibre_db.session.delete(del_cc)
|
||||||
|
calibre_db.session.commit()
|
||||||
elif c.datatype == 'rating':
|
elif c.datatype == 'rating':
|
||||||
del_cc = getattr(book, cc_string)[0]
|
del_cc = getattr(book, cc_string)[0]
|
||||||
getattr(book, cc_string).remove(del_cc)
|
getattr(book, cc_string).remove(del_cc)
|
||||||
if len(del_cc.books) == 0:
|
if len(del_cc.books) == 0:
|
||||||
db.session.delete(del_cc)
|
log.debug('remove ' + str(c.id))
|
||||||
|
calibre_db.session.delete(del_cc)
|
||||||
|
calibre_db.session.commit()
|
||||||
else:
|
else:
|
||||||
del_cc = getattr(book, cc_string)[0]
|
del_cc = getattr(book, cc_string)[0]
|
||||||
getattr(book, cc_string).remove(del_cc)
|
getattr(book, cc_string).remove(del_cc)
|
||||||
db.session.delete(del_cc)
|
log.debug('remove ' + str(c.id))
|
||||||
|
calibre_db.session.delete(del_cc)
|
||||||
|
calibre_db.session.commit()
|
||||||
else:
|
else:
|
||||||
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],
|
||||||
db.session, 'custom')
|
calibre_db.session, 'custom')
|
||||||
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:
|
else:
|
||||||
db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format).delete()
|
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
|
||||||
db.session.commit()
|
filter(db.Data.format == book_format).delete()
|
||||||
|
calibre_db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(e)
|
||||||
|
calibre_db.session.rollback()
|
||||||
else:
|
else:
|
||||||
# book not found
|
# book not found
|
||||||
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
||||||
if book_format:
|
if book_format:
|
||||||
|
flash(_('Book Format Successfully Deleted'), category="success")
|
||||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
else:
|
else:
|
||||||
|
flash(_('Book Successfully Deleted'), category="success")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
def render_edit_book(book_id):
|
def render_edit_book(book_id):
|
||||||
db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
cc = 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 = db.session.query(db.Books)\
|
book = calibre_db.get_filtered_book(book_id)
|
||||||
.filter(db.Books.id == book_id).filter(common_filters()).first()
|
|
||||||
|
|
||||||
if not book:
|
if not book:
|
||||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
@@ -217,7 +260,7 @@ def render_edit_book(book_id):
|
|||||||
for lang in book.languages:
|
for lang in book.languages:
|
||||||
lang.language_name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
lang.language_name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||||
|
|
||||||
book = order_authors(book)
|
book = calibre_db.order_authors(book)
|
||||||
|
|
||||||
author_names = []
|
author_names = []
|
||||||
for authr in book.authors:
|
for authr in book.authors:
|
||||||
@@ -225,27 +268,129 @@ def render_edit_book(book_id):
|
|||||||
|
|
||||||
# Option for showing convertbook button
|
# Option for showing convertbook button
|
||||||
valid_source_formats=list()
|
valid_source_formats=list()
|
||||||
if config.config_ebookconverter == 2:
|
allowed_conversion_formats = list()
|
||||||
|
kepub_possible=None
|
||||||
|
if config.config_converterpath:
|
||||||
for file in book.data:
|
for file in book.data:
|
||||||
if file.format.lower() in constants.EXTENSIONS_CONVERT:
|
if file.format.lower() in constants.EXTENSIONS_CONVERT:
|
||||||
valid_source_formats.append(file.format.lower())
|
valid_source_formats.append(file.format.lower())
|
||||||
|
if config.config_kepubifypath and 'epub' in [file.format.lower() for file in book.data]:
|
||||||
|
kepub_possible = True
|
||||||
|
if not config.config_converterpath:
|
||||||
|
valid_source_formats.append('epub')
|
||||||
|
|
||||||
# Determine what formats don't already exist
|
# Determine what formats don't already exist
|
||||||
allowed_conversion_formats = constants.EXTENSIONS_CONVERT.copy()
|
if config.config_converterpath:
|
||||||
|
allowed_conversion_formats = constants.EXTENSIONS_CONVERT[:]
|
||||||
for file in book.data:
|
for file in book.data:
|
||||||
try:
|
if file.format.lower() in allowed_conversion_formats:
|
||||||
allowed_conversion_formats.remove(file.format.lower())
|
allowed_conversion_formats.remove(file.format.lower())
|
||||||
except Exception:
|
if kepub_possible:
|
||||||
log.warning('%s already removed from list.', file.format.lower())
|
allowed_conversion_formats.append('kepub')
|
||||||
|
|
||||||
return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
|
return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
|
||||||
title=_(u"edit metadata"), page="editbook",
|
title=_(u"edit metadata"), page="editbook",
|
||||||
conversion_formats=allowed_conversion_formats,
|
conversion_formats=allowed_conversion_formats,
|
||||||
|
config=config,
|
||||||
source_formats=valid_source_formats)
|
source_formats=valid_source_formats)
|
||||||
|
|
||||||
|
|
||||||
|
def edit_book_ratings(to_save, book):
|
||||||
|
changed = False
|
||||||
|
if to_save["rating"].strip():
|
||||||
|
old_rating = False
|
||||||
|
if len(book.ratings) > 0:
|
||||||
|
old_rating = book.ratings[0].rating
|
||||||
|
ratingx2 = int(float(to_save["rating"]) * 2)
|
||||||
|
if ratingx2 != old_rating:
|
||||||
|
changed = True
|
||||||
|
is_rating = calibre_db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first()
|
||||||
|
if is_rating:
|
||||||
|
book.ratings.append(is_rating)
|
||||||
|
else:
|
||||||
|
new_rating = db.Ratings(rating=ratingx2)
|
||||||
|
book.ratings.append(new_rating)
|
||||||
|
if old_rating:
|
||||||
|
book.ratings.remove(book.ratings[0])
|
||||||
|
else:
|
||||||
|
if len(book.ratings) > 0:
|
||||||
|
book.ratings.remove(book.ratings[0])
|
||||||
|
changed = True
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def edit_book_tags(tags, book):
|
||||||
|
input_tags = tags.split(',')
|
||||||
|
input_tags = list(map(lambda it: it.strip(), input_tags))
|
||||||
|
# Remove duplicates
|
||||||
|
input_tags = helper.uniq(input_tags)
|
||||||
|
return modify_database_object(input_tags, book.tags, db.Tags, calibre_db.session, 'tags')
|
||||||
|
|
||||||
|
|
||||||
|
def edit_book_series(series, book):
|
||||||
|
input_series = [series.strip()]
|
||||||
|
input_series = [x for x in input_series if x != '']
|
||||||
|
return modify_database_object(input_series, book.series, db.Series, calibre_db.session, 'series')
|
||||||
|
|
||||||
|
|
||||||
|
def edit_book_series_index(series_index, book):
|
||||||
|
# Add default series_index to book
|
||||||
|
modif_date = False
|
||||||
|
series_index = series_index or '1'
|
||||||
|
if book.series_index != series_index:
|
||||||
|
book.series_index = series_index
|
||||||
|
modif_date = True
|
||||||
|
return modif_date
|
||||||
|
|
||||||
|
# Handle book comments/description
|
||||||
|
def edit_book_comments(comments, book):
|
||||||
|
modif_date = False
|
||||||
|
if len(book.comments):
|
||||||
|
if book.comments[0].text != comments:
|
||||||
|
book.comments[0].text = comments
|
||||||
|
modif_date = True
|
||||||
|
else:
|
||||||
|
if comments:
|
||||||
|
book.comments.append(db.Comments(text=comments, book=book.id))
|
||||||
|
modif_date = True
|
||||||
|
return modif_date
|
||||||
|
|
||||||
|
|
||||||
|
def edit_book_languages(languages, book, upload=False):
|
||||||
|
input_languages = languages.split(',')
|
||||||
|
unknown_languages = []
|
||||||
|
if not upload:
|
||||||
|
input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages)
|
||||||
|
else:
|
||||||
|
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
|
||||||
|
for l in unknown_languages:
|
||||||
|
log.error('%s is not a valid language', l)
|
||||||
|
flash(_(u"%(langname)s is not a valid language", langname=l), category="warning")
|
||||||
|
# ToDo: Not working correct
|
||||||
|
if upload and len(input_l) == 1:
|
||||||
|
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
|
||||||
|
# the book it's language is set to the filter language
|
||||||
|
if input_l[0] != current_user.filter_language() and current_user.filter_language() != "all":
|
||||||
|
input_l[0] = calibre_db.session.query(db.Languages). \
|
||||||
|
filter(db.Languages.lang_code == current_user.filter_language()).first()
|
||||||
|
# Remove duplicates
|
||||||
|
input_l = helper.uniq(input_l)
|
||||||
|
return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages')
|
||||||
|
|
||||||
|
|
||||||
|
def edit_book_publisher(to_save, book):
|
||||||
|
changed = False
|
||||||
|
if to_save["publisher"]:
|
||||||
|
publisher = to_save["publisher"].rstrip().strip()
|
||||||
|
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,
|
||||||
|
'publisher')
|
||||||
|
elif len(book.publishers):
|
||||||
|
changed |= modify_database_object([], book.publishers, db.Publishers, calibre_db.session, 'publisher')
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def edit_cc_data(book_id, book, to_save):
|
def edit_cc_data(book_id, book, to_save):
|
||||||
cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
changed = False
|
||||||
|
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
for c in cc:
|
for c in cc:
|
||||||
cc_string = "custom_column_" + str(c.id)
|
cc_string = "custom_column_" + str(c.id)
|
||||||
if not c.is_multiple:
|
if not c.is_multiple:
|
||||||
@@ -254,7 +399,7 @@ def edit_cc_data(book_id, book, to_save):
|
|||||||
else:
|
else:
|
||||||
cc_db_value = None
|
cc_db_value = None
|
||||||
if to_save[cc_string].strip():
|
if to_save[cc_string].strip():
|
||||||
if c.datatype == 'int' or c.datatype == 'bool':
|
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':
|
||||||
@@ -264,14 +409,17 @@ def edit_cc_data(book_id, book, to_save):
|
|||||||
if cc_db_value is not None:
|
if cc_db_value is not None:
|
||||||
if to_save[cc_string] is not None:
|
if to_save[cc_string] is not None:
|
||||||
setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string])
|
setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string])
|
||||||
|
changed = True
|
||||||
else:
|
else:
|
||||||
del_cc = getattr(book, cc_string)[0]
|
del_cc = getattr(book, cc_string)[0]
|
||||||
getattr(book, cc_string).remove(del_cc)
|
getattr(book, cc_string).remove(del_cc)
|
||||||
db.session.delete(del_cc)
|
calibre_db.session.delete(del_cc)
|
||||||
|
changed = True
|
||||||
else:
|
else:
|
||||||
cc_class = db.cc_classes[c.id]
|
cc_class = db.cc_classes[c.id]
|
||||||
new_cc = cc_class(value=to_save[cc_string], book=book_id)
|
new_cc = cc_class(value=to_save[cc_string], book=book_id)
|
||||||
db.session.add(new_cc)
|
calibre_db.session.add(new_cc)
|
||||||
|
changed = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if c.datatype == 'rating':
|
if c.datatype == 'rating':
|
||||||
@@ -282,16 +430,18 @@ def edit_cc_data(book_id, book, to_save):
|
|||||||
del_cc = getattr(book, cc_string)[0]
|
del_cc = getattr(book, cc_string)[0]
|
||||||
getattr(book, cc_string).remove(del_cc)
|
getattr(book, cc_string).remove(del_cc)
|
||||||
if len(del_cc.books) == 0:
|
if len(del_cc.books) == 0:
|
||||||
db.session.delete(del_cc)
|
calibre_db.session.delete(del_cc)
|
||||||
|
changed = True
|
||||||
cc_class = db.cc_classes[c.id]
|
cc_class = db.cc_classes[c.id]
|
||||||
new_cc = db.session.query(cc_class).filter(
|
new_cc = calibre_db.session.query(cc_class).filter(
|
||||||
cc_class.value == to_save[cc_string].strip()).first()
|
cc_class.value == to_save[cc_string].strip()).first()
|
||||||
# if no cc val is found add it
|
# if no cc val is found add it
|
||||||
if new_cc is None:
|
if new_cc is None:
|
||||||
new_cc = cc_class(value=to_save[cc_string].strip())
|
new_cc = cc_class(value=to_save[cc_string].strip())
|
||||||
db.session.add(new_cc)
|
calibre_db.session.add(new_cc)
|
||||||
db.session.flush()
|
changed = True
|
||||||
new_cc = db.session.query(cc_class).filter(
|
calibre_db.session.flush()
|
||||||
|
new_cc = calibre_db.session.query(cc_class).filter(
|
||||||
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)
|
||||||
@@ -301,13 +451,17 @@ def edit_cc_data(book_id, book, to_save):
|
|||||||
del_cc = getattr(book, cc_string)[0]
|
del_cc = getattr(book, cc_string)[0]
|
||||||
getattr(book, cc_string).remove(del_cc)
|
getattr(book, cc_string).remove(del_cc)
|
||||||
if not del_cc.books or len(del_cc.books) == 0:
|
if not del_cc.books or len(del_cc.books) == 0:
|
||||||
db.session.delete(del_cc)
|
calibre_db.session.delete(del_cc)
|
||||||
|
changed = True
|
||||||
else:
|
else:
|
||||||
input_tags = to_save[cc_string].split(',')
|
input_tags = to_save[cc_string].split(',')
|
||||||
input_tags = list(map(lambda it: it.strip(), input_tags))
|
input_tags = list(map(lambda it: it.strip(), input_tags))
|
||||||
modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session,
|
changed |= modify_database_object(input_tags,
|
||||||
|
getattr(book, cc_string),
|
||||||
|
db.cc_classes[c.id],
|
||||||
|
calibre_db.session,
|
||||||
'custom')
|
'custom')
|
||||||
return cc
|
return changed
|
||||||
|
|
||||||
def upload_single_file(request, book, book_id):
|
def upload_single_file(request, book, book_id):
|
||||||
# Check and handle Uploaded file
|
# Check and handle Uploaded file
|
||||||
@@ -315,9 +469,11 @@ def upload_single_file(request, book, book_id):
|
|||||||
requested_file = request.files['btn-upload-format']
|
requested_file = request.files['btn-upload-format']
|
||||||
# check for empty request
|
# check for empty request
|
||||||
if requested_file.filename != '':
|
if requested_file.filename != '':
|
||||||
|
if not current_user.role_upload():
|
||||||
|
abort(403)
|
||||||
if '.' in requested_file.filename:
|
if '.' in requested_file.filename:
|
||||||
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
||||||
if file_ext not in constants.EXTENSIONS_UPLOAD:
|
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),
|
flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext),
|
||||||
category="error")
|
category="error")
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
@@ -343,17 +499,22 @@ 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))
|
||||||
|
|
||||||
file_size = os.path.getsize(saved_filename)
|
file_size = os.path.getsize(saved_filename)
|
||||||
is_format = db.session.query(db.Data).filter(db.Data.book == book_id).\
|
is_format = calibre_db.get_book_format(book_id, file_ext.upper())
|
||||||
filter(db.Data.format == file_ext.upper()).first()
|
|
||||||
|
|
||||||
# Format entry already exists, no need to update the database
|
# Format entry already exists, no need to update the database
|
||||||
if is_format:
|
if is_format:
|
||||||
log.warning('Book format %s already existing', file_ext.upper())
|
log.warning('Book format %s already existing', file_ext.upper())
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
|
db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
|
||||||
db.session.add(db_format)
|
calibre_db.session.add(db_format)
|
||||||
db.session.commit()
|
calibre_db.session.commit()
|
||||||
db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
|
except OperationalError as e:
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
log.error('Database error: %s', e)
|
||||||
|
flash(_(u"Database error: %(error)s.", error=e), category="error")
|
||||||
|
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)
|
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
|
||||||
@@ -361,7 +522,8 @@ def upload_single_file(request, book, book_id):
|
|||||||
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")
|
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")
|
||||||
|
|
||||||
return uploader.process(
|
return uploader.process(
|
||||||
saved_filename, *os.path.splitext(requested_file.filename))
|
saved_filename, *os.path.splitext(requested_file.filename),
|
||||||
|
rarExecutable=config.config_rarfile_location)
|
||||||
|
|
||||||
|
|
||||||
def upload_cover(request, book):
|
def upload_cover(request, book):
|
||||||
@@ -369,11 +531,13 @@ def upload_cover(request, book):
|
|||||||
requested_file = request.files['btn-upload-cover']
|
requested_file = request.files['btn-upload-cover']
|
||||||
# check for empty request
|
# check for empty request
|
||||||
if requested_file.filename != '':
|
if requested_file.filename != '':
|
||||||
if helper.save_cover(requested_file, book.path) is True:
|
if not current_user.role_upload():
|
||||||
|
abort(403)
|
||||||
|
ret, message = helper.save_cover(requested_file, book.path)
|
||||||
|
if ret is True:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# ToDo Message not always coorect
|
flash(message, category="error")
|
||||||
flash(_(u"Cover is not a supported imageformat (jpg/png/webp), can't save"), category="error")
|
|
||||||
return False
|
return False
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -382,14 +546,14 @@ def upload_cover(request, book):
|
|||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
def edit_book(book_id):
|
def edit_book(book_id):
|
||||||
|
modif_date = False
|
||||||
# Show form
|
# Show form
|
||||||
if request.method != 'POST':
|
if request.method != 'POST':
|
||||||
return render_edit_book(book_id)
|
return render_edit_book(book_id)
|
||||||
|
|
||||||
# create the function for sorting...
|
# create the function for sorting...
|
||||||
db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
book = db.session.query(db.Books)\
|
book = calibre_db.get_filtered_book(book_id)
|
||||||
.filter(db.Books.id == book_id).filter(common_filters()).first()
|
|
||||||
|
|
||||||
# Book not found
|
# Book not found
|
||||||
if not book:
|
if not book:
|
||||||
@@ -399,6 +563,7 @@ def edit_book(book_id):
|
|||||||
meta = upload_single_file(request, book, book_id)
|
meta = upload_single_file(request, book, book_id)
|
||||||
if upload_cover(request, book) is True:
|
if upload_cover(request, book) is True:
|
||||||
book.has_cover = 1
|
book.has_cover = 1
|
||||||
|
modif_date = True
|
||||||
try:
|
try:
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
merge_metadata(to_save, meta)
|
merge_metadata(to_save, meta)
|
||||||
@@ -410,21 +575,24 @@ def edit_book(book_id):
|
|||||||
to_save["book_title"] = _(u'Unknown')
|
to_save["book_title"] = _(u'Unknown')
|
||||||
book.title = to_save["book_title"].rstrip().strip()
|
book.title = to_save["book_title"].rstrip().strip()
|
||||||
edited_books_id = book.id
|
edited_books_id = book.id
|
||||||
|
modif_date = True
|
||||||
|
|
||||||
# handle author(s)
|
# handle author(s)
|
||||||
input_authors = to_save["author_name"].split('&')
|
input_authors = to_save["author_name"].split('&')
|
||||||
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
|
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
|
# we have all author names now
|
||||||
if input_authors == ['']:
|
if input_authors == ['']:
|
||||||
input_authors = [_(u'Unknown')] # prevent empty Author
|
input_authors = [_(u'Unknown')] # prevent empty Author
|
||||||
|
|
||||||
modify_database_object(input_authors, book.authors, db.Authors, db.session, '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
|
# 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
|
# everything then is assembled for sorted author field in database
|
||||||
sort_authors_list = list()
|
sort_authors_list = list()
|
||||||
for inp in input_authors:
|
for inp in input_authors:
|
||||||
stored_author = db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
||||||
if not stored_author:
|
if not stored_author:
|
||||||
stored_author = helper.get_sorted_author(inp)
|
stored_author = helper.get_sorted_author(inp)
|
||||||
else:
|
else:
|
||||||
@@ -434,7 +602,7 @@ def edit_book(book_id):
|
|||||||
if book.author_sort != sort_authors:
|
if book.author_sort != sort_authors:
|
||||||
edited_books_id = book.id
|
edited_books_id = book.id
|
||||||
book.author_sort = sort_authors
|
book.author_sort = sort_authors
|
||||||
|
modif_date = True
|
||||||
|
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
@@ -444,79 +612,59 @@ def edit_book(book_id):
|
|||||||
error = helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0])
|
error = helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0])
|
||||||
|
|
||||||
if not error:
|
if not error:
|
||||||
|
if "cover_url" in to_save:
|
||||||
if to_save["cover_url"]:
|
if to_save["cover_url"]:
|
||||||
if helper.save_cover_from_url(to_save["cover_url"], book.path) is True:
|
if not current_user.role_upload():
|
||||||
|
return "", (403)
|
||||||
|
result, error = helper.save_cover_from_url(to_save["cover_url"], book.path)
|
||||||
|
if result is True:
|
||||||
book.has_cover = 1
|
book.has_cover = 1
|
||||||
|
modif_date = True
|
||||||
else:
|
else:
|
||||||
flash(_(u"Cover is not a jpg file, can't save"), category="error")
|
flash(error, category="error")
|
||||||
|
|
||||||
if book.series_index != to_save["series_index"]:
|
# Add default series_index to book
|
||||||
book.series_index = to_save["series_index"]
|
modif_date |= edit_book_series_index(to_save["series_index"], book)
|
||||||
|
|
||||||
# Handle book comments/description
|
# Handle book comments/description
|
||||||
if len(book.comments):
|
modif_date |= edit_book_comments(to_save["description"], book)
|
||||||
book.comments[0].text = to_save["description"]
|
|
||||||
else:
|
|
||||||
book.comments.append(db.Comments(text=to_save["description"], book=book.id))
|
|
||||||
|
|
||||||
|
# Handle identifiers
|
||||||
|
input_identifiers = identifier_list(to_save, book)
|
||||||
|
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
|
||||||
|
if warning:
|
||||||
|
flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning")
|
||||||
|
modif_date |= modification
|
||||||
# Handle book tags
|
# Handle book tags
|
||||||
input_tags = to_save["tags"].split(',')
|
modif_date |= edit_book_tags(to_save['tags'], book)
|
||||||
input_tags = list(map(lambda it: it.strip(), input_tags))
|
|
||||||
modify_database_object(input_tags, book.tags, db.Tags, db.session, 'tags')
|
|
||||||
|
|
||||||
# Handle book series
|
# Handle book series
|
||||||
input_series = [to_save["series"].strip()]
|
modif_date |= edit_book_series(to_save["series"], book)
|
||||||
input_series = [x for x in input_series if x != '']
|
|
||||||
modify_database_object(input_series, book.series, db.Series, db.session, 'series')
|
|
||||||
|
|
||||||
if to_save["pubdate"]:
|
if to_save["pubdate"]:
|
||||||
try:
|
try:
|
||||||
book.pubdate = datetime.datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
|
book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
book.pubdate = db.Books.DEFAULT_PUBDATE
|
book.pubdate = db.Books.DEFAULT_PUBDATE
|
||||||
else:
|
else:
|
||||||
book.pubdate = db.Books.DEFAULT_PUBDATE
|
book.pubdate = db.Books.DEFAULT_PUBDATE
|
||||||
|
|
||||||
if to_save["publisher"]:
|
# handle book publisher
|
||||||
publisher = to_save["publisher"].rstrip().strip()
|
modif_date |= edit_book_publisher(to_save, book)
|
||||||
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
|
|
||||||
modify_database_object([publisher], book.publishers, db.Publishers, db.session, 'publisher')
|
|
||||||
elif len(book.publishers):
|
|
||||||
modify_database_object([], book.publishers, db.Publishers, db.session, 'publisher')
|
|
||||||
|
|
||||||
|
|
||||||
# handle book languages
|
# handle book languages
|
||||||
input_languages = to_save["languages"].split(',')
|
modif_date |= edit_book_languages(to_save['languages'], book)
|
||||||
unknown_languages = []
|
|
||||||
input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages)
|
|
||||||
for l in unknown_languages:
|
|
||||||
log.error('%s is not a valid language', l)
|
|
||||||
flash(_(u"%(langname)s is not a valid language", langname=l), category="error")
|
|
||||||
modify_database_object(list(input_l), book.languages, db.Languages, db.session, 'languages')
|
|
||||||
|
|
||||||
# handle book ratings
|
# handle book ratings
|
||||||
if to_save["rating"].strip():
|
modif_date |= edit_book_ratings(to_save, book)
|
||||||
old_rating = False
|
|
||||||
if len(book.ratings) > 0:
|
|
||||||
old_rating = book.ratings[0].rating
|
|
||||||
ratingx2 = int(float(to_save["rating"]) * 2)
|
|
||||||
if ratingx2 != old_rating:
|
|
||||||
is_rating = db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first()
|
|
||||||
if is_rating:
|
|
||||||
book.ratings.append(is_rating)
|
|
||||||
else:
|
|
||||||
new_rating = db.Ratings(rating=ratingx2)
|
|
||||||
book.ratings.append(new_rating)
|
|
||||||
if old_rating:
|
|
||||||
book.ratings.remove(book.ratings[0])
|
|
||||||
else:
|
|
||||||
if len(book.ratings) > 0:
|
|
||||||
book.ratings.remove(book.ratings[0])
|
|
||||||
|
|
||||||
# handle cc data
|
# handle cc data
|
||||||
edit_cc_data(book_id, book, to_save)
|
modif_date |= edit_cc_data(book_id, book, to_save)
|
||||||
|
|
||||||
db.session.commit()
|
if modif_date:
|
||||||
|
book.last_modified = datetime.utcnow()
|
||||||
|
calibre_db.session.merge(book)
|
||||||
|
calibre_db.session.commit()
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
if "detail_view" in to_save:
|
if "detail_view" in to_save:
|
||||||
@@ -525,12 +673,12 @@ def edit_book(book_id):
|
|||||||
flash(_("Metadata successfully updated"), category="success")
|
flash(_("Metadata successfully updated"), category="success")
|
||||||
return render_edit_book(book_id)
|
return render_edit_book(book_id)
|
||||||
else:
|
else:
|
||||||
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 e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
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))
|
||||||
|
|
||||||
@@ -548,6 +696,19 @@ 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):
|
||||||
|
"""Generate a list of Identifiers from form information"""
|
||||||
|
id_type_prefix = 'identifier-type-'
|
||||||
|
id_val_prefix = 'identifier-val-'
|
||||||
|
result = []
|
||||||
|
for type_key, type_value in to_save.items():
|
||||||
|
if not type_key.startswith(id_type_prefix):
|
||||||
|
continue
|
||||||
|
val_key = id_val_prefix + type_key[len(id_type_prefix):]
|
||||||
|
if val_key not in to_save.keys():
|
||||||
|
continue
|
||||||
|
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
|
||||||
|
return result
|
||||||
|
|
||||||
@editbook.route("/upload", methods=["GET", "POST"])
|
@editbook.route("/upload", methods=["GET", "POST"])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@@ -557,14 +718,16 @@ def upload():
|
|||||||
abort(404)
|
abort(404)
|
||||||
if request.method == 'POST' and 'btn-upload' in request.files:
|
if request.method == 'POST' and 'btn-upload' in request.files:
|
||||||
for requested_file in request.files.getlist("btn-upload"):
|
for requested_file in request.files.getlist("btn-upload"):
|
||||||
|
try:
|
||||||
|
modif_date = False
|
||||||
# create the function for sorting...
|
# create the function for sorting...
|
||||||
db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
|
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
|
||||||
|
|
||||||
# check if file extension is correct
|
# check if file extension is correct
|
||||||
if '.' in requested_file.filename:
|
if '.' in requested_file.filename:
|
||||||
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
||||||
if file_ext not in constants.EXTENSIONS_UPLOAD:
|
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
|
||||||
flash(
|
flash(
|
||||||
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
|
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
|
||||||
ext=file_ext), category="error")
|
ext=file_ext), category="error")
|
||||||
@@ -575,7 +738,7 @@ def upload():
|
|||||||
|
|
||||||
# extract metadata from file
|
# extract metadata from file
|
||||||
try:
|
try:
|
||||||
meta = uploader.upload(requested_file)
|
meta = uploader.upload(requested_file, config.config_rarfile_location)
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
log.error("File %s could not saved to temp dir", requested_file.filename)
|
log.error("File %s could not saved to temp dir", requested_file.filename)
|
||||||
flash(_(u"File %(filename)s could not saved to temp dir",
|
flash(_(u"File %(filename)s could not saved to temp dir",
|
||||||
@@ -583,21 +746,47 @@ def upload():
|
|||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
title = meta.title
|
title = meta.title
|
||||||
authr = meta.author
|
authr = meta.author
|
||||||
tags = meta.tags
|
|
||||||
series = meta.series
|
|
||||||
series_index = meta.series_id
|
|
||||||
title_dir = helper.get_valid_filename(title)
|
|
||||||
author_dir = helper.get_valid_filename(authr)
|
|
||||||
filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir)
|
|
||||||
saved_filename = os.path.join(filepath, title_dir + meta.extension.lower())
|
|
||||||
|
|
||||||
if title != _(u'Unknown') and authr != _(u'Unknown'):
|
if title != _(u'Unknown') and authr != _(u'Unknown'):
|
||||||
entry = helper.check_exists_book(authr, title)
|
entry = calibre_db.check_exists_book(authr, title)
|
||||||
if entry:
|
if entry:
|
||||||
log.info("Uploaded book probably exists in library")
|
log.info("Uploaded book probably exists in library")
|
||||||
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
|
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
|
||||||
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
|
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
|
||||||
|
|
||||||
|
# handle authors
|
||||||
|
input_authors = authr.split('&')
|
||||||
|
# handle_authors(input_authors)
|
||||||
|
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
|
||||||
|
|
||||||
|
sort_authors_list=list()
|
||||||
|
db_author = None
|
||||||
|
for inp in input_authors:
|
||||||
|
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
||||||
|
if not stored_author:
|
||||||
|
if not db_author:
|
||||||
|
db_author = db.Authors(inp, helper.get_sorted_author(inp), "")
|
||||||
|
calibre_db.session.add(db_author)
|
||||||
|
calibre_db.session.commit()
|
||||||
|
sort_author = helper.get_sorted_author(inp)
|
||||||
|
else:
|
||||||
|
if not db_author:
|
||||||
|
db_author = stored_author
|
||||||
|
sort_author = stored_author.sort
|
||||||
|
sort_authors_list.append(sort_author) # helper.get_sorted_author(sort_author))
|
||||||
|
sort_authors = ' & '.join(sort_authors_list)
|
||||||
|
|
||||||
|
title_dir = helper.get_valid_filename(title)
|
||||||
|
author_dir = helper.get_valid_filename(db_author.name)
|
||||||
|
filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir)
|
||||||
|
saved_filename = os.path.join(filepath, title_dir + meta.extension.lower())
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -608,16 +797,11 @@ def upload():
|
|||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
try:
|
try:
|
||||||
copyfile(meta.file_path, saved_filename)
|
copyfile(meta.file_path, saved_filename)
|
||||||
except OSError:
|
|
||||||
log.error("Failed to store file %s (Permission denied)", saved_filename)
|
|
||||||
flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error")
|
|
||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
|
||||||
try:
|
|
||||||
os.unlink(meta.file_path)
|
os.unlink(meta.file_path)
|
||||||
except OSError:
|
except OSError as e:
|
||||||
log.error("Failed to delete file %(file)s (Permission denied)", meta.file_path)
|
log.error("Failed to move file %s: %s", saved_filename, e)
|
||||||
flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path),
|
flash(_(u"Failed to Move File %(file)s: %(error)s", file=saved_filename, error=e), category="error")
|
||||||
category="warning")
|
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
|
|
||||||
if meta.cover is None:
|
if meta.cover is None:
|
||||||
has_cover = 0
|
has_cover = 0
|
||||||
@@ -625,108 +809,84 @@ def upload():
|
|||||||
os.path.join(filepath, "cover.jpg"))
|
os.path.join(filepath, "cover.jpg"))
|
||||||
else:
|
else:
|
||||||
has_cover = 1
|
has_cover = 1
|
||||||
move(meta.cover, os.path.join(filepath, "cover.jpg"))
|
|
||||||
|
|
||||||
# handle authors
|
|
||||||
is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first()
|
|
||||||
if is_author:
|
|
||||||
db_author = is_author
|
|
||||||
else:
|
|
||||||
db_author = db.Authors(authr, helper.get_sorted_author(authr), "")
|
|
||||||
db.session.add(db_author)
|
|
||||||
|
|
||||||
# handle series
|
|
||||||
db_series = None
|
|
||||||
is_series = db.session.query(db.Series).filter(db.Series.name == series).first()
|
|
||||||
if is_series:
|
|
||||||
db_series = is_series
|
|
||||||
elif series != '':
|
|
||||||
db_series = db.Series(series, "")
|
|
||||||
db.session.add(db_series)
|
|
||||||
|
|
||||||
# add language actually one value in list
|
|
||||||
input_language = meta.languages
|
|
||||||
db_language = None
|
|
||||||
if input_language != "":
|
|
||||||
input_language = isoLanguages.get(name=input_language).part3
|
|
||||||
hasLanguage = db.session.query(db.Languages).filter(db.Languages.lang_code == input_language).first()
|
|
||||||
if hasLanguage:
|
|
||||||
db_language = hasLanguage
|
|
||||||
else:
|
|
||||||
db_language = db.Languages(input_language)
|
|
||||||
db.session.add(db_language)
|
|
||||||
|
|
||||||
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
|
|
||||||
# the book it's language is set to the filter language
|
|
||||||
if db_language != current_user.filter_language() and current_user.filter_language() != "all":
|
|
||||||
db_language = db.session.query(db.Languages).\
|
|
||||||
filter(db.Languages.lang_code == current_user.filter_language()).first()
|
|
||||||
|
|
||||||
# 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('\\', '/')
|
||||||
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1),
|
# Calibre adds books with utc as timezone
|
||||||
series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language)
|
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
|
||||||
db_book.authors.append(db_author)
|
'1', datetime.utcnow(), path, has_cover, db_author, [], "")
|
||||||
if db_series:
|
|
||||||
db_book.series.append(db_series)
|
modif_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
|
||||||
if db_language is not None:
|
'author')
|
||||||
db_book.languages.append(db_language)
|
|
||||||
file_size = os.path.getsize(saved_filename)
|
# Add series_index to book
|
||||||
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir)
|
modif_date |= edit_book_series_index(meta.series_id, db_book)
|
||||||
|
|
||||||
|
# add languages
|
||||||
|
modif_date |= edit_book_languages(meta.languages, db_book, upload=True)
|
||||||
|
|
||||||
# handle tags
|
# handle tags
|
||||||
input_tags = tags.split(',')
|
modif_date |= edit_book_tags(meta.tags, db_book)
|
||||||
input_tags = list(map(lambda it: it.strip(), input_tags))
|
|
||||||
if input_tags[0] !="":
|
# handle series
|
||||||
modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags')
|
modif_date |= edit_book_series(meta.series, db_book)
|
||||||
|
|
||||||
|
# Add file to book
|
||||||
|
file_size = os.path.getsize(saved_filename)
|
||||||
|
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir)
|
||||||
|
db_book.data.append(db_data)
|
||||||
|
calibre_db.session.add(db_book)
|
||||||
|
|
||||||
# flush content, get db_book.id available
|
# flush content, get db_book.id available
|
||||||
db_book.data.append(db_data)
|
calibre_db.session.flush()
|
||||||
db.session.add(db_book)
|
|
||||||
db.session.flush()
|
# Comments needs book id therfore only possiblw after flush
|
||||||
|
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
|
||||||
|
|
||||||
# add comment
|
|
||||||
book_id = db_book.id
|
book_id = db_book.id
|
||||||
upload_comment = Markup(meta.description).unescape()
|
title = db_book.title
|
||||||
if upload_comment != "":
|
|
||||||
db.session.add(db.Comments(upload_comment, book_id))
|
error = helper.update_dir_stucture(book_id, config.config_calibre_dir, input_authors[0])
|
||||||
|
|
||||||
|
# move cover to final directory, including book id
|
||||||
|
if has_cover:
|
||||||
|
new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg")
|
||||||
|
try:
|
||||||
|
copyfile(meta.cover, new_coverpath)
|
||||||
|
os.unlink(meta.cover)
|
||||||
|
except OSError as e:
|
||||||
|
log.error("Failed to move cover file %s: %s", new_coverpath, e)
|
||||||
|
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath,
|
||||||
|
error=e),
|
||||||
|
category="error")
|
||||||
|
|
||||||
# save data to database, reread data
|
# save data to database, reread data
|
||||||
db.session.commit()
|
calibre_db.session.commit()
|
||||||
db.update_title_sort(config)
|
#calibre_db.setup_db(config, ub.app_DB_path)
|
||||||
# Reread book. It's important not to filter the result, as it could have language which hide it from
|
# Reread book. It's important not to filter the result, as it could have language which hide it from
|
||||||
# current users view (tags are not stored/extracted from metadata and could also be limited)
|
# current users view (tags are not stored/extracted from metadata and could also be limited)
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
#book = calibre_db.get_book(book_id)
|
||||||
|
|
||||||
# upload book to gdrive if nesseccary and add "(bookid)" to folder name
|
|
||||||
if config.config_use_google_drive:
|
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
|
||||||
error = helper.update_dir_stucture(book.id, config.config_calibre_dir)
|
|
||||||
db.session.commit()
|
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
if error:
|
if error:
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
uploadText=_(u"File %(file)s uploaded", file=book.title)
|
uploadText=_(u"File %(file)s uploaded", file=title)
|
||||||
worker.add_upload(current_user.nickname,
|
worker.add_upload(current_user.nickname,
|
||||||
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")
|
"<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>")
|
||||||
|
|
||||||
# create data for displaying display Full language name instead of iso639.part3language
|
|
||||||
if db_language is not None:
|
|
||||||
book.languages[0].language_name = _(meta.languages)
|
|
||||||
author_names = []
|
|
||||||
for author in db_book.authors:
|
|
||||||
author_names.append(author.name)
|
|
||||||
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():
|
||||||
resp = {"location": url_for('editbook.edit_book', book_id=db_book.id)}
|
resp = {"location": url_for('editbook.edit_book', book_id=book_id)}
|
||||||
return Response(json.dumps(resp), mimetype='application/json')
|
return Response(json.dumps(resp), mimetype='application/json')
|
||||||
else:
|
else:
|
||||||
resp = {"location": url_for('web.show_book', book_id=db_book.id)}
|
resp = {"location": url_for('web.show_book', book_id=book_id)}
|
||||||
return Response(json.dumps(resp), mimetype='application/json')
|
return Response(json.dumps(resp), mimetype='application/json')
|
||||||
|
except OperationalError as e:
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
log.error("Database error: %s", e)
|
||||||
|
flash(_(u"Database error: %(error)s.", error=e), category="error")
|
||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
|
@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
|
|||||||
+9
-12
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -23,6 +22,7 @@ import zipfile
|
|||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from . import isoLanguages
|
from . import isoLanguages
|
||||||
|
from .helper import split_authors
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
|
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
|
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
|
||||||
if len(tmp) > 0:
|
if len(tmp) > 0:
|
||||||
if s == 'creator':
|
if s == 'creator':
|
||||||
epub_metadata[s] = ' & '.join(p.xpath('dc:%s/text()' % s, namespaces=ns))
|
epub_metadata[s] = ' & '.join(split_authors(p.xpath('dc:%s/text()' % s, namespaces=ns)))
|
||||||
elif s == 'subject':
|
elif s == 'subject':
|
||||||
epub_metadata[s] = ', '.join(p.xpath('dc:%s/text()' % s, namespaces=ns))
|
epub_metadata[s] = ', '.join(p.xpath('dc:%s/text()' % s, namespaces=ns))
|
||||||
else:
|
else:
|
||||||
@@ -83,16 +83,8 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
else:
|
else:
|
||||||
epub_metadata['description'] = ""
|
epub_metadata['description'] = ""
|
||||||
|
|
||||||
if epub_metadata['language'] == u'Unknown':
|
|
||||||
epub_metadata['language'] = ""
|
|
||||||
else:
|
|
||||||
lang = epub_metadata['language'].split('-', 1)[0].lower()
|
lang = epub_metadata['language'].split('-', 1)[0].lower()
|
||||||
if len(lang) == 2:
|
epub_metadata['language'] = isoLanguages.get_lang3(lang)
|
||||||
epub_metadata['language'] = isoLanguages.get(part1=lang).name
|
|
||||||
elif len(lang) == 3:
|
|
||||||
epub_metadata['language'] = isoLanguages.get(part3=lang).name
|
|
||||||
else:
|
|
||||||
epub_metadata['language'] = ""
|
|
||||||
|
|
||||||
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
|
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
|
||||||
if len(series) > 0:
|
if len(series) > 0:
|
||||||
@@ -123,8 +115,13 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
markupTree = etree.fromstring(markup)
|
markupTree = etree.fromstring(markup)
|
||||||
# no matter xhtml or html with no namespace
|
# no matter xhtml or html with no namespace
|
||||||
imgsrc = markupTree.xpath("//*[local-name() = 'img']/@src")
|
imgsrc = markupTree.xpath("//*[local-name() = 'img']/@src")
|
||||||
|
# Alternative image source
|
||||||
|
if not len(imgsrc):
|
||||||
|
imgsrc = markupTree.xpath("//attribute::*[contains(local-name(), 'href')]")
|
||||||
|
if len(imgsrc):
|
||||||
# imgsrc maybe startwith "../"" so fullpath join then relpath to cwd
|
# imgsrc maybe startwith "../"" so fullpath join then relpath to cwd
|
||||||
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(coverpath, coversection[0])), imgsrc[0]))
|
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(coverpath, coversection[0])),
|
||||||
|
imgsrc[0]))
|
||||||
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)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
|||||||
+12
-14
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -35,18 +34,17 @@ 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
|
||||||
|
|
||||||
try:
|
from . import logger, gdriveutils, config, ub, calibre_db
|
||||||
from googleapiclient.errors import HttpError
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
from . import logger, gdriveutils, config, db
|
|
||||||
from .web import admin_required
|
from .web import admin_required
|
||||||
|
|
||||||
|
|
||||||
gdrive = Blueprint('gdrive', __name__)
|
gdrive = Blueprint('gdrive', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from googleapiclient.errors import HttpError
|
||||||
|
except ImportError as err:
|
||||||
|
log.debug("Cannot import googleapiclient, using GDrive will not work: %s", 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'
|
||||||
@@ -74,7 +72,7 @@ def google_drive_callback():
|
|||||||
credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code)
|
credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code)
|
||||||
with open(gdriveutils.CREDENTIALS, 'w') as f:
|
with open(gdriveutils.CREDENTIALS, 'w') as f:
|
||||||
f.write(credentials.to_json())
|
f.write(credentials.to_json())
|
||||||
except ValueError as error:
|
except (ValueError, AttributeError) as error:
|
||||||
log.error(error)
|
log.error(error)
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
@@ -95,8 +93,7 @@ def watch_gdrive():
|
|||||||
try:
|
try:
|
||||||
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
|
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
|
||||||
'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000)
|
'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000)
|
||||||
config.config_google_drive_watch_changes_response = json.dumps(result)
|
config.config_google_drive_watch_changes_response = result
|
||||||
# after save(), config_google_drive_watch_changes_response will be a json object, not string
|
|
||||||
config.save()
|
config.save()
|
||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
reason=json.loads(e.content)['error']['errors'][0]
|
reason=json.loads(e.content)['error']['errors'][0]
|
||||||
@@ -119,7 +116,7 @@ def revoke_watch_gdrive():
|
|||||||
last_watch_response['resourceId'])
|
last_watch_response['resourceId'])
|
||||||
except HttpError:
|
except HttpError:
|
||||||
pass
|
pass
|
||||||
config.config_google_drive_watch_changes_response = None
|
config.config_google_drive_watch_changes_response = {}
|
||||||
config.save()
|
config.save()
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
@@ -146,7 +143,8 @@ def on_received_watch_confirmation():
|
|||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
|
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||||
else:
|
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' and response['file']['md5Checksum'] != hashlib.md5(dbpath):
|
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||||
|
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
|
||||||
tmpDir = tempfile.gettempdir()
|
tmpDir = tempfile.gettempdir()
|
||||||
log.info('Database file updated')
|
log.info('Database file updated')
|
||||||
copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time())))
|
copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time())))
|
||||||
@@ -155,7 +153,7 @@ def on_received_watch_confirmation():
|
|||||||
log.info('Setting up new DB')
|
log.info('Setting up new DB')
|
||||||
# prevent error on windows, as os.rename does on exisiting files
|
# prevent error on windows, as os.rename does on exisiting files
|
||||||
move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath)
|
move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath)
|
||||||
db.setup_db(config)
|
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
updateMetaData()
|
updateMetaData()
|
||||||
|
|||||||
+26
-4
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -28,14 +27,18 @@ 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
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pydrive.auth import GoogleAuth
|
from pydrive.auth import GoogleAuth
|
||||||
from pydrive.drive import GoogleDrive
|
from pydrive.drive import GoogleDrive
|
||||||
from pydrive.auth import RefreshError
|
from pydrive.auth import RefreshError
|
||||||
from apiclient import errors
|
from apiclient import errors
|
||||||
|
from httplib2 import ServerNotFoundError
|
||||||
gdrive_support = True
|
gdrive_support = True
|
||||||
except ImportError:
|
importError = None
|
||||||
|
except ImportError as err:
|
||||||
|
importError = err
|
||||||
gdrive_support = False
|
gdrive_support = False
|
||||||
|
|
||||||
from . import logger, cli, config
|
from . import logger, cli, config
|
||||||
@@ -51,6 +54,8 @@ if gdrive_support:
|
|||||||
logger.get('googleapiclient.discovery_cache').setLevel(logger.logging.ERROR)
|
logger.get('googleapiclient.discovery_cache').setLevel(logger.logging.ERROR)
|
||||||
if not logger.is_debug_enabled():
|
if not logger.is_debug_enabled():
|
||||||
logger.get('googleapiclient.discovery').setLevel(logger.logging.ERROR)
|
logger.get('googleapiclient.discovery').setLevel(logger.logging.ERROR)
|
||||||
|
else:
|
||||||
|
log.debug("Cannot import pydrive,httplib2, using gdrive will not work: %s", importError)
|
||||||
|
|
||||||
|
|
||||||
class Singleton:
|
class Singleton:
|
||||||
@@ -98,7 +103,11 @@ class Singleton:
|
|||||||
@Singleton
|
@Singleton
|
||||||
class Gauth:
|
class Gauth:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
try:
|
||||||
self.auth = GoogleAuth(settings_file=SETTINGS_YAML)
|
self.auth = GoogleAuth(settings_file=SETTINGS_YAML)
|
||||||
|
except NameError as error:
|
||||||
|
log.error(error)
|
||||||
|
self.auth = None
|
||||||
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -193,14 +202,18 @@ def getDrive(drive=None, gauth=None):
|
|||||||
return drive
|
return drive
|
||||||
|
|
||||||
def listRootFolders():
|
def listRootFolders():
|
||||||
|
try:
|
||||||
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 as e:
|
||||||
|
log.info("GDrive Error %s" % e)
|
||||||
|
fileList = []
|
||||||
return fileList
|
return fileList
|
||||||
|
|
||||||
|
|
||||||
def getEbooksFolder(drive):
|
def getEbooksFolder(drive):
|
||||||
return getFolderInFolder('root',config.config_google_drive_folder,drive)
|
return getFolderInFolder('root', config.config_google_drive_folder, drive)
|
||||||
|
|
||||||
|
|
||||||
def getFolderInFolder(parentId, folderName, drive):
|
def getFolderInFolder(parentId, folderName, drive):
|
||||||
@@ -230,7 +243,7 @@ def getEbooksFolderId(drive=None):
|
|||||||
gDriveId.path = '/'
|
gDriveId.path = '/'
|
||||||
session.merge(gDriveId)
|
session.merge(gDriveId)
|
||||||
session.commit()
|
session.commit()
|
||||||
return
|
return gDriveId.gdrive_id
|
||||||
|
|
||||||
|
|
||||||
def getFile(pathId, fileName, drive):
|
def getFile(pathId, fileName, drive):
|
||||||
@@ -475,8 +488,13 @@ def getChangeById (drive, change_id):
|
|||||||
|
|
||||||
# Deletes the local hashes database to force search for new folder names
|
# Deletes the local hashes database to force search for new folder names
|
||||||
def deleteDatabaseOnChange():
|
def deleteDatabaseOnChange():
|
||||||
|
try:
|
||||||
session.query(GdriveId).delete()
|
session.query(GdriveId).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except (OperationalError, InvalidRequestError):
|
||||||
|
session.rollback()
|
||||||
|
log.info(u"GDrive DB is not Writeable")
|
||||||
|
|
||||||
|
|
||||||
def updateGdriveCalibreFromLocal():
|
def updateGdriveCalibreFromLocal():
|
||||||
copyToDrive(Gdrive.Instance().drive, config.config_calibre_dir, False, True)
|
copyToDrive(Gdrive.Instance().drive, config.config_calibre_dir, False, True)
|
||||||
@@ -584,8 +602,12 @@ def get_error_text(client_secrets=None):
|
|||||||
if not os.path.isfile(CLIENT_SECRETS):
|
if not os.path.isfile(CLIENT_SECRETS):
|
||||||
return 'client_secrets.json is missing or not readable'
|
return 'client_secrets.json is missing or not readable'
|
||||||
|
|
||||||
|
try:
|
||||||
with open(CLIENT_SECRETS, 'r') as settings:
|
with open(CLIENT_SECRETS, 'r') as settings:
|
||||||
filedata = json.load(settings)
|
filedata = json.load(settings)
|
||||||
|
except PermissionError:
|
||||||
|
return 'client_secrets.json is missing or not readable'
|
||||||
|
|
||||||
if 'web' not in filedata:
|
if 'web' not in filedata:
|
||||||
return 'client_secrets.json is not configured for web application'
|
return 'client_secrets.json is not configured for web application'
|
||||||
if 'redirect_uris' not in filedata['web']:
|
if 'redirect_uris' not in filedata['web']:
|
||||||
|
|||||||
+227
-221
@@ -21,9 +21,7 @@ from __future__ import division, print_function, unicode_literals
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
@@ -32,16 +30,15 @@ from datetime import datetime, timedelta
|
|||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from babel import Locale as LC
|
|
||||||
from babel.core import UnknownLocaleError
|
|
||||||
from babel.dates import format_datetime
|
from babel.dates import format_datetime
|
||||||
from babel.units import format_unit
|
from babel.units import format_unit
|
||||||
from flask import send_from_directory, make_response, redirect, abort
|
from flask import send_from_directory, make_response, redirect, abort
|
||||||
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_, or_, text, func
|
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 . import calibre_db
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@@ -56,14 +53,14 @@ except ImportError:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
|
from PIL import UnidentifiedImageError
|
||||||
use_PIL = True
|
use_PIL = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
use_PIL = False
|
use_PIL = False
|
||||||
|
|
||||||
from . import logger, config, get_locale, db, ub, isoLanguages, worker
|
from . import logger, config, get_locale, db, ub, worker
|
||||||
from . import gdriveutils as gd
|
from . import gdriveutils as gd
|
||||||
from .constants import STATIC_DIR as _STATIC_DIR
|
from .constants import STATIC_DIR as _STATIC_DIR
|
||||||
from .pagination import Pagination
|
|
||||||
from .subproc_wrapper import process_wait
|
from .subproc_wrapper import process_wait
|
||||||
from .worker import STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
|
from .worker import STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
|
||||||
from .worker import TASK_EMAIL, TASK_CONVERT, TASK_UPLOAD, TASK_CONVERT_ANY
|
from .worker import TASK_EMAIL, TASK_CONVERT, TASK_UPLOAD, TASK_CONVERT_ANY
|
||||||
@@ -74,8 +71,8 @@ log = logger.create()
|
|||||||
|
|
||||||
# Convert existing book entry to new format
|
# Convert existing book entry to new format
|
||||||
def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None):
|
def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None):
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
book = calibre_db.get_book(book_id)
|
||||||
data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == old_book_format).first()
|
data = calibre_db.get_book_format(book.id, old_book_format)
|
||||||
if not data:
|
if not data:
|
||||||
error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
|
error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
|
||||||
log.error("convert_book_format: %s", error_message)
|
log.error("convert_book_format: %s", error_message)
|
||||||
@@ -101,10 +98,10 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
|||||||
# text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title)
|
# text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title)
|
||||||
else:
|
else:
|
||||||
settings = dict()
|
settings = dict()
|
||||||
text = (u"%s -> %s: %s" % (old_book_format, new_book_format, book.title))
|
txt = (u"%s -> %s: %s" % (old_book_format, new_book_format, book.title))
|
||||||
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
|
||||||
worker.add_convert(file_path, book.id, user_id, text, settings, kindle_mail)
|
worker.add_convert(file_path, book.id, user_id, txt, settings, kindle_mail)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
error_message = _(u"%(format)s not found: %(fn)s",
|
error_message = _(u"%(format)s not found: %(fn)s",
|
||||||
@@ -141,36 +138,53 @@ def check_send_to_kindle(entry):
|
|||||||
returns all available book formats for sending to Kindle
|
returns all available book formats for sending to Kindle
|
||||||
"""
|
"""
|
||||||
if len(entry.data):
|
if len(entry.data):
|
||||||
bookformats=list()
|
bookformats = list()
|
||||||
if config.config_ebookconverter == 0:
|
if not config.config_converterpath:
|
||||||
# no converter - only for mobi and pdf formats
|
# no converter - only for mobi and pdf formats
|
||||||
for ele in iter(entry.data):
|
for ele in iter(entry.data):
|
||||||
|
if ele.uncompressed_size < config.mail_size:
|
||||||
if 'MOBI' in ele.format:
|
if 'MOBI' in ele.format:
|
||||||
bookformats.append({'format':'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')})
|
bookformats.append({'format': 'Mobi',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Mobi')})
|
||||||
if 'PDF' in ele.format:
|
if 'PDF' in ele.format:
|
||||||
bookformats.append({'format':'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')})
|
bookformats.append({'format': 'Pdf',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
||||||
if 'AZW' in ele.format:
|
if 'AZW' in ele.format:
|
||||||
bookformats.append({'format':'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')})
|
bookformats.append({'format': 'Azw',
|
||||||
'''if 'AZW3' in ele.format:
|
'convert': 0,
|
||||||
bookformats.append({'format':'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')})'''
|
'text': _('Send %(format)s to Kindle', format='Azw')})
|
||||||
else:
|
else:
|
||||||
formats = list()
|
formats = list()
|
||||||
for ele in iter(entry.data):
|
for ele in iter(entry.data):
|
||||||
|
if ele.uncompressed_size < config.mail_size:
|
||||||
formats.append(ele.format)
|
formats.append(ele.format)
|
||||||
if 'MOBI' in formats:
|
if 'MOBI' in formats:
|
||||||
bookformats.append({'format': 'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')})
|
bookformats.append({'format': 'Mobi',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Mobi')})
|
||||||
if 'AZW' in formats:
|
if 'AZW' in formats:
|
||||||
bookformats.append({'format': 'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')})
|
bookformats.append({'format': 'Azw',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Azw')})
|
||||||
if 'PDF' in formats:
|
if 'PDF' in formats:
|
||||||
bookformats.append({'format': 'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')})
|
bookformats.append({'format': 'Pdf',
|
||||||
if config.config_ebookconverter >= 1:
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
||||||
|
if config.config_converterpath:
|
||||||
if 'EPUB' in formats and not 'MOBI' in formats:
|
if 'EPUB' in formats and not 'MOBI' in formats:
|
||||||
bookformats.append({'format': 'Mobi','convert':1,
|
bookformats.append({'format': 'Mobi',
|
||||||
'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Mobi')})
|
'convert':1,
|
||||||
if config.config_ebookconverter == 2:
|
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
|
||||||
|
orig='Epub',
|
||||||
|
format='Mobi')})
|
||||||
if 'AZW3' in formats and not 'MOBI' in formats:
|
if 'AZW3' in formats and not 'MOBI' in formats:
|
||||||
bookformats.append({'format': 'Mobi','convert':2,
|
bookformats.append({'format': 'Mobi',
|
||||||
'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Azw3',format='Mobi')})
|
'convert': 2,
|
||||||
|
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
|
||||||
|
orig='Azw3',
|
||||||
|
format='Mobi')})
|
||||||
return bookformats
|
return bookformats
|
||||||
else:
|
else:
|
||||||
log.error(u'Cannot find book entry %d', entry.id)
|
log.error(u'Cannot find book entry %d', entry.id)
|
||||||
@@ -195,7 +209,7 @@ def check_read_formats(entry):
|
|||||||
# 3: If Pdf file is existing, it's directly send to kindle email
|
# 3: If Pdf file is existing, it's directly send to kindle email
|
||||||
def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
|
def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
|
||||||
"""Send email with attachments"""
|
"""Send email with attachments"""
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
book = calibre_db.get_book(book_id)
|
||||||
|
|
||||||
if convert == 1:
|
if convert == 1:
|
||||||
# returns None if success, otherwise errormessage
|
# returns None if success, otherwise errormessage
|
||||||
@@ -204,7 +218,6 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
|
|||||||
# returns None if success, otherwise errormessage
|
# returns None if success, otherwise errormessage
|
||||||
return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, kindle_mail)
|
return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, kindle_mail)
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -224,22 +237,22 @@ 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:
|
||||||
value = (unidecode.unidecode(value)).strip()
|
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
|
if isinstance(value, str): # Python3 str, Python2 unicode
|
||||||
value = re_slugify.sub('', value).strip()
|
value = re_slugify.sub('', value)
|
||||||
else:
|
else:
|
||||||
value = unicode(re_slugify.sub('', value).strip())
|
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)
|
||||||
# pipe has to be replaced with comma
|
# pipe has to be replaced with comma
|
||||||
value = re.sub(r'[\|]+', u',', value, flags=re.U)
|
value = re.sub(r'[|]+', u',', value, flags=re.U)
|
||||||
value = value[:128]
|
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:
|
if sys.version_info.major == 3:
|
||||||
@@ -248,6 +261,22 @@ def get_valid_filename(value, replace_whitespace=True):
|
|||||||
return value.decode('utf-8')
|
return value.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def split_authors(values):
|
||||||
|
authors_list = []
|
||||||
|
for value in values:
|
||||||
|
authors = re.split('[&;]', value)
|
||||||
|
for author in authors:
|
||||||
|
commas = author.count(',')
|
||||||
|
if commas == 1:
|
||||||
|
author_split = author.split(',')
|
||||||
|
authors_list.append(author_split[1].strip() + ' ' + author_split[0].strip())
|
||||||
|
elif commas > 1:
|
||||||
|
authors_list.extend([x.strip() for x in author.split(',')])
|
||||||
|
else:
|
||||||
|
authors_list.append(author.strip())
|
||||||
|
return authors_list
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_author(value):
|
def get_sorted_author(value):
|
||||||
try:
|
try:
|
||||||
if ',' not in value:
|
if ',' not in value:
|
||||||
@@ -255,7 +284,10 @@ def get_sorted_author(value):
|
|||||||
combined = "(" + ")|(".join(regexes) + ")"
|
combined = "(" + ")|(".join(regexes) + ")"
|
||||||
value = value.split(" ")
|
value = value.split(" ")
|
||||||
if re.match(combined, value[-1].upper()):
|
if re.match(combined, value[-1].upper()):
|
||||||
|
if len(value) > 1:
|
||||||
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
|
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
|
||||||
|
else:
|
||||||
|
value2 = value[0]
|
||||||
elif len(value) == 1:
|
elif len(value) == 1:
|
||||||
value2 = value[0]
|
value2 = value[0]
|
||||||
else:
|
else:
|
||||||
@@ -264,6 +296,9 @@ def get_sorted_author(value):
|
|||||||
value2 = value
|
value2 = value
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error("Sorting author %s failed: %s", value, ex)
|
log.error("Sorting author %s failed: %s", value, ex)
|
||||||
|
if isinstance(list, value2):
|
||||||
|
value2 = value[0]
|
||||||
|
else:
|
||||||
value2 = value
|
value2 = value
|
||||||
return value2
|
return value2
|
||||||
|
|
||||||
@@ -277,20 +312,39 @@ def delete_book_file(book, calibrepath, book_format=None):
|
|||||||
for file in os.listdir(path):
|
for file in os.listdir(path):
|
||||||
if file.upper().endswith("."+book_format):
|
if file.upper().endswith("."+book_format):
|
||||||
os.remove(os.path.join(path, file))
|
os.remove(os.path.join(path, file))
|
||||||
|
return True, None
|
||||||
else:
|
else:
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
if len(next(os.walk(path))[1]):
|
try:
|
||||||
log.error("Deleting book %s failed, path has subfolders: %s", book.id, book.path)
|
for root, folders, files in os.walk(path):
|
||||||
return False
|
for f in files:
|
||||||
shutil.rmtree(path, ignore_errors=True)
|
os.unlink(os.path.join(root, f))
|
||||||
return True
|
if len(folders):
|
||||||
|
log.warning("Deleting book {} failed, path {} has subfolders: {}".format(book.id,
|
||||||
|
book.path, folders))
|
||||||
|
return True, _("Deleting bookfolder for book %(id)s failed, path has subfolders: %(path)s",
|
||||||
|
id=book.id,
|
||||||
|
path=book.path)
|
||||||
|
shutil.rmtree(path)
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
log.error("Deleting book %s failed: %s", book.id, e)
|
||||||
|
return False, _("Deleting book %(id)s failed: %(message)s", id=book.id, message=e)
|
||||||
|
authorpath = os.path.join(calibrepath, os.path.split(book.path)[0])
|
||||||
|
if not os.listdir(authorpath):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(authorpath)
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
log.error("Deleting authorpath for book %s failed: %s", book.id, e)
|
||||||
|
return True, None
|
||||||
else:
|
else:
|
||||||
log.error("Deleting book %s failed, book path not valid: %s", book.id, book.path)
|
log.error("Deleting book %s failed, book path not valid: %s", book.id, book.path)
|
||||||
return False
|
return True, _("Deleting book %(id)s, book path not valid: %(path)s",
|
||||||
|
id=book.id,
|
||||||
|
path=book.path)
|
||||||
|
|
||||||
|
|
||||||
def update_dir_structure_file(book_id, calibrepath, first_author):
|
def update_dir_structure_file(book_id, calibrepath, first_author):
|
||||||
localbook = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
localbook = calibre_db.get_book(book_id)
|
||||||
path = os.path.join(calibrepath, localbook.path)
|
path = os.path.join(calibrepath, localbook.path)
|
||||||
|
|
||||||
authordir = localbook.path.split('/')[0]
|
authordir = localbook.path.split('/')[0]
|
||||||
@@ -303,16 +357,16 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
|
|||||||
new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")"
|
new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")"
|
||||||
|
|
||||||
if titledir != new_titledir:
|
if titledir != new_titledir:
|
||||||
try:
|
|
||||||
new_title_path = os.path.join(os.path.dirname(path), new_titledir)
|
new_title_path = os.path.join(os.path.dirname(path), new_titledir)
|
||||||
|
try:
|
||||||
if not os.path.exists(new_title_path):
|
if not os.path.exists(new_title_path):
|
||||||
os.renames(path, new_title_path)
|
os.renames(os.path.normcase(path), os.path.normcase(new_title_path))
|
||||||
else:
|
else:
|
||||||
log.info("Copying title: %s into existing: %s", path, new_title_path)
|
log.info("Copying title: %s into existing: %s", path, new_title_path)
|
||||||
for dir_name, __, file_list in os.walk(path):
|
for dir_name, __, file_list in os.walk(path):
|
||||||
for file in file_list:
|
for file in file_list:
|
||||||
os.renames(os.path.join(dir_name, file),
|
os.renames(os.path.normcase(os.path.join(dir_name, file)),
|
||||||
os.path.join(new_title_path + dir_name[len(path):], file))
|
os.path.normcase(os.path.join(new_title_path + dir_name[len(path):], file)))
|
||||||
path = new_title_path
|
path = new_title_path
|
||||||
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
|
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
@@ -321,9 +375,9 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
|
|||||||
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",
|
||||||
src=path, dest=new_title_path, error=str(ex))
|
src=path, dest=new_title_path, error=str(ex))
|
||||||
if authordir != new_authordir:
|
if authordir != new_authordir:
|
||||||
try:
|
|
||||||
new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path))
|
new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path))
|
||||||
os.renames(path, new_author_path)
|
try:
|
||||||
|
os.renames(os.path.normcase(path), os.path.normcase(new_author_path))
|
||||||
localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
|
localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
log.error("Rename author from: %s to %s: %s", path, new_author_path, ex)
|
log.error("Rename author from: %s to %s: %s", path, new_author_path, ex)
|
||||||
@@ -332,12 +386,14 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
|
|||||||
src=path, dest=new_author_path, error=str(ex))
|
src=path, dest=new_author_path, error=str(ex))
|
||||||
# Rename all files from old names to new names
|
# Rename all files from old names to new names
|
||||||
if authordir != new_authordir or titledir != new_titledir:
|
if authordir != new_authordir or titledir != new_titledir:
|
||||||
|
new_name = ""
|
||||||
try:
|
try:
|
||||||
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
|
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
|
||||||
path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
|
path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
|
||||||
for file_format in localbook.data:
|
for file_format in localbook.data:
|
||||||
os.renames(os.path.join(path_name, file_format.name + '.' + file_format.format.lower()),
|
os.renames(os.path.normcase(
|
||||||
os.path.join(path_name, new_name + '.' + file_format.format.lower()))
|
os.path.join(path_name, file_format.name + '.' + file_format.format.lower())),
|
||||||
|
os.path.normcase(os.path.join(path_name, new_name + '.' + file_format.format.lower())))
|
||||||
file_format.name = new_name
|
file_format.name = new_name
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
log.error("Rename file in path %s to %s: %s", path, new_name, ex)
|
log.error("Rename file in path %s to %s: %s", path, new_name, ex)
|
||||||
@@ -349,7 +405,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
|
|||||||
|
|
||||||
def update_dir_structure_gdrive(book_id, first_author):
|
def update_dir_structure_gdrive(book_id, first_author):
|
||||||
error = False
|
error = False
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
book = calibre_db.get_book(book_id)
|
||||||
path = book.path
|
path = book.path
|
||||||
|
|
||||||
authordir = book.path.split('/')[0]
|
authordir = book.path.split('/')[0]
|
||||||
@@ -395,7 +451,7 @@ def update_dir_structure_gdrive(book_id, first_author):
|
|||||||
|
|
||||||
|
|
||||||
def delete_book_gdrive(book, book_format):
|
def delete_book_gdrive(book, book_format):
|
||||||
error= False
|
error = None
|
||||||
if book_format:
|
if book_format:
|
||||||
name = ''
|
name = ''
|
||||||
for entry in book.data:
|
for entry in book.data:
|
||||||
@@ -403,38 +459,53 @@ def delete_book_gdrive(book, book_format):
|
|||||||
name = entry.name + '.' + book_format
|
name = entry.name + '.' + book_format
|
||||||
gFile = gd.getFileFromEbooksFolder(book.path, name)
|
gFile = gd.getFileFromEbooksFolder(book.path, name)
|
||||||
else:
|
else:
|
||||||
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path),book.path.split('/')[1])
|
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), book.path.split('/')[1])
|
||||||
if gFile:
|
if gFile:
|
||||||
gd.deleteDatabaseEntry(gFile['id'])
|
gd.deleteDatabaseEntry(gFile['id'])
|
||||||
gFile.Trash()
|
gFile.Trash()
|
||||||
else:
|
else:
|
||||||
error =_(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
|
error = _(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
|
||||||
return error
|
|
||||||
|
return error is None, error
|
||||||
|
|
||||||
|
|
||||||
def reset_password(user_id):
|
def reset_password(user_id):
|
||||||
existing_user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
|
existing_user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
|
||||||
|
if not existing_user:
|
||||||
|
return 0, None
|
||||||
|
if not config.get_mail_server_configured():
|
||||||
|
return 2, None
|
||||||
|
try:
|
||||||
password = generate_random_password()
|
password = generate_random_password()
|
||||||
existing_user.password = generate_password_hash(password)
|
existing_user.password = generate_password_hash(password)
|
||||||
if not config.get_mail_server_configured():
|
|
||||||
return (2, None)
|
|
||||||
try:
|
|
||||||
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.nickname, password, True)
|
||||||
return (1, existing_user.nickname)
|
return 1, existing_user.nickname
|
||||||
except Exception:
|
except Exception:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
return (0, None)
|
return 0, None
|
||||||
|
|
||||||
|
|
||||||
def generate_random_password():
|
def generate_random_password():
|
||||||
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
||||||
passlen = 8
|
passlen = 8
|
||||||
return "".join(random.sample(s,passlen ))
|
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))
|
||||||
|
|
||||||
################################## External interface
|
|
||||||
|
|
||||||
def update_dir_stucture(book_id, calibrepath, first_author = None):
|
def uniq(inpt):
|
||||||
|
output = []
|
||||||
|
for x in inpt:
|
||||||
|
if x not in output:
|
||||||
|
output.append(x)
|
||||||
|
return output
|
||||||
|
|
||||||
|
# ################################# External interface #################################
|
||||||
|
|
||||||
|
|
||||||
|
def update_dir_stucture(book_id, calibrepath, first_author=None):
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
return update_dir_structure_gdrive(book_id, first_author)
|
return update_dir_structure_gdrive(book_id, first_author)
|
||||||
else:
|
else:
|
||||||
@@ -448,38 +519,64 @@ def delete_book(book, calibrepath, book_format):
|
|||||||
return delete_book_file(book, calibrepath, book_format)
|
return delete_book_file(book, calibrepath, book_format)
|
||||||
|
|
||||||
|
|
||||||
def get_book_cover(book_id):
|
def get_cover_on_failure(use_generic_cover):
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
if use_generic_cover:
|
||||||
if book.has_cover:
|
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_book_cover(book_id):
|
||||||
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
|
return get_book_cover_internal(book, use_generic_cover_on_failure=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_book_cover_with_uuid(book_uuid,
|
||||||
|
use_generic_cover_on_failure=True):
|
||||||
|
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||||
|
return get_book_cover_internal(book, use_generic_cover_on_failure)
|
||||||
|
|
||||||
|
|
||||||
|
def get_book_cover_internal(book, use_generic_cover_on_failure):
|
||||||
|
if book and book.has_cover:
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
try:
|
try:
|
||||||
if not gd.is_gdrive_ready():
|
if not gd.is_gdrive_ready():
|
||||||
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
path=gd.get_cover_via_gdrive(book.path)
|
path = gd.get_cover_via_gdrive(book.path)
|
||||||
if path:
|
if path:
|
||||||
return redirect(path)
|
return redirect(path)
|
||||||
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 send_from_directory(_STATIC_DIR, "generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
# traceback.print_exc()
|
# traceback.print_exc()
|
||||||
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
|
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)
|
||||||
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
|
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
|
||||||
return send_from_directory(cover_file_path, "cover.jpg")
|
return send_from_directory(cover_file_path, "cover.jpg")
|
||||||
else:
|
else:
|
||||||
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
else:
|
else:
|
||||||
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
|
|
||||||
|
|
||||||
# saves book cover from url
|
# saves book cover from url
|
||||||
def save_cover_from_url(url, book_path):
|
def save_cover_from_url(url, book_path):
|
||||||
img = requests.get(url)
|
try:
|
||||||
|
img = requests.get(url, timeout=(10, 200)) # ToDo: Error Handling
|
||||||
|
img.raise_for_status()
|
||||||
return save_cover(img, book_path)
|
return save_cover(img, book_path)
|
||||||
|
except (requests.exceptions.HTTPError,
|
||||||
|
requests.exceptions.ConnectionError,
|
||||||
|
requests.exceptions.Timeout) as ex:
|
||||||
|
log.info(u'Cover Download Error %s', ex)
|
||||||
|
return False, _("Error Downloading Cover")
|
||||||
|
except UnidentifiedImageError as ex:
|
||||||
|
log.info(u'File Format Error %s', ex)
|
||||||
|
return False, _("Cover Format Error")
|
||||||
|
|
||||||
|
|
||||||
def save_cover_from_filestorage(filepath, saved_filename, img):
|
def save_cover_from_filestorage(filepath, saved_filename, img):
|
||||||
@@ -494,16 +591,13 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
|||||||
os.makedirs(filepath)
|
os.makedirs(filepath)
|
||||||
except OSError:
|
except OSError:
|
||||||
log.error(u"Failed to create path for cover")
|
log.error(u"Failed to create path for cover")
|
||||||
return False
|
return False, _(u"Failed to create path for cover")
|
||||||
try:
|
try:
|
||||||
img.save(os.path.join(filepath, saved_filename))
|
img.save(os.path.join(filepath, saved_filename))
|
||||||
except IOError:
|
except (IOError, OSError):
|
||||||
log.error(u"Cover-file is not a valid image file")
|
log.error(u"Cover-file is not a valid image file, or could not be stored")
|
||||||
return False
|
return False, _(u"Cover-file is not a valid image file, or could not be stored")
|
||||||
except OSError:
|
return True, None
|
||||||
log.error(u"Failed to store cover-file")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# saves book cover to gdrive or locally
|
# saves book cover to gdrive or locally
|
||||||
@@ -513,10 +607,10 @@ def save_cover(img, book_path):
|
|||||||
if use_PIL:
|
if use_PIL:
|
||||||
if content_type not in ('image/jpeg', 'image/png', 'image/webp'):
|
if content_type not in ('image/jpeg', 'image/png', 'image/webp'):
|
||||||
log.error("Only jpg/jpeg/png/webp files are supported as coverfile")
|
log.error("Only jpg/jpeg/png/webp files are supported as coverfile")
|
||||||
return False
|
return False, _("Only jpg/jpeg/png/webp files are supported as coverfile")
|
||||||
# convert to jpg because calibre only supports jpg
|
# convert to jpg because calibre only supports jpg
|
||||||
if content_type in ('image/png', 'image/webp'):
|
if content_type in ('image/png', 'image/webp'):
|
||||||
if hasattr(img,'stream'):
|
if hasattr(img, 'stream'):
|
||||||
imgc = PILImage.open(img.stream)
|
imgc = PILImage.open(img.stream)
|
||||||
else:
|
else:
|
||||||
imgc = PILImage.open(io.BytesIO(img.content))
|
imgc = PILImage.open(io.BytesIO(img.content))
|
||||||
@@ -525,25 +619,25 @@ def save_cover(img, book_path):
|
|||||||
im.save(tmp_bytesio, format='JPEG')
|
im.save(tmp_bytesio, format='JPEG')
|
||||||
img._content = tmp_bytesio.getvalue()
|
img._content = tmp_bytesio.getvalue()
|
||||||
else:
|
else:
|
||||||
if content_type not in ('image/jpeg'):
|
if content_type not in 'image/jpeg':
|
||||||
log.error("Only jpg/jpeg files are supported as coverfile")
|
log.error("Only jpg/jpeg files are supported as coverfile")
|
||||||
return False
|
return False, _("Only jpg/jpeg files are supported as coverfile")
|
||||||
|
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
tmpDir = gettempdir()
|
tmpDir = gettempdir()
|
||||||
if save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) is True:
|
ret, message = save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img)
|
||||||
|
if ret is True:
|
||||||
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'),
|
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'),
|
||||||
os.path.join(tmpDir, "uploaded_cover.jpg"))
|
os.path.join(tmpDir, "uploaded_cover.jpg"))
|
||||||
log.info("Cover is saved on Google Drive")
|
log.info("Cover is saved on Google Drive")
|
||||||
return True
|
return True, None
|
||||||
else:
|
else:
|
||||||
return False
|
return False, message
|
||||||
else:
|
else:
|
||||||
return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img)
|
return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img)
|
||||||
|
|
||||||
|
|
||||||
|
def do_download_file(book, book_format, client, data, headers):
|
||||||
def do_download_file(book, book_format, data, headers):
|
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
startTime = time.time()
|
startTime = time.time()
|
||||||
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
|
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
|
||||||
@@ -557,42 +651,47 @@ def do_download_file(book, book_format, data, headers):
|
|||||||
if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)):
|
if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)):
|
||||||
# ToDo: improve error handling
|
# ToDo: improve error handling
|
||||||
log.error('File not found: %s', os.path.join(filename, data.name + "." + book_format))
|
log.error('File not found: %s', os.path.join(filename, data.name + "." + book_format))
|
||||||
|
|
||||||
|
if client == "kobo" and book_format == "kepub":
|
||||||
|
headers["Content-Disposition"] = headers["Content-Disposition"].replace(".kepub", ".kepub.epub")
|
||||||
|
|
||||||
response = make_response(send_from_directory(filename, data.name + "." + book_format))
|
response = make_response(send_from_directory(filename, data.name + "." + book_format))
|
||||||
response.headers = headers
|
# ToDo Check headers parameter
|
||||||
|
for element in headers:
|
||||||
|
response.headers[element[0]] = element[1]
|
||||||
return response
|
return response
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def check_unrar(unrarLocation):
|
def check_unrar(unrarLocation):
|
||||||
if not unrarLocation:
|
if not unrarLocation:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not os.path.exists(unrarLocation):
|
if not os.path.exists(unrarLocation):
|
||||||
return 'Unrar binary file not found'
|
return _('Unrar binary file not found')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
unrarLocation = unrarLocation.encode(sys.getfilesystemencoding())
|
unrarLocation = unrarLocation.encode(sys.getfilesystemencoding())
|
||||||
unrarLocation = [unrarLocation]
|
unrarLocation = [unrarLocation]
|
||||||
for lines in process_wait(unrarLocation):
|
for lines in process_wait(unrarLocation):
|
||||||
value = re.search('UNRAR (.*) freeware', lines)
|
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)
|
||||||
except OSError as err:
|
break
|
||||||
|
except (OSError, UnicodeDecodeError) as err:
|
||||||
log.exception(err)
|
log.exception(err)
|
||||||
return 'Error excecuting UnRar'
|
return _('Error excecuting UnRar')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def json_serial(obj):
|
def json_serial(obj):
|
||||||
"""JSON serializer for objects not serializable by default json code"""
|
"""JSON serializer for objects not serializable by default json code"""
|
||||||
|
|
||||||
if isinstance(obj, (datetime)):
|
if isinstance(obj, datetime):
|
||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
if isinstance(obj, (timedelta)):
|
if isinstance(obj, timedelta):
|
||||||
return {
|
return {
|
||||||
'__type__': 'timedelta',
|
'__type__': 'timedelta',
|
||||||
'days': obj.days,
|
'days': obj.days,
|
||||||
@@ -600,7 +699,7 @@ def json_serial(obj):
|
|||||||
'microseconds': obj.microseconds,
|
'microseconds': obj.microseconds,
|
||||||
}
|
}
|
||||||
# return obj.isoformat()
|
# return obj.isoformat()
|
||||||
raise TypeError ("Type %s not serializable" % type(obj))
|
raise TypeError("Type %s not serializable" % type(obj))
|
||||||
|
|
||||||
|
|
||||||
# helper function for displaying the runtime of tasks
|
# helper function for displaying the runtime of tasks
|
||||||
@@ -622,7 +721,7 @@ 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 task in tasklist:
|
for task in tasklist:
|
||||||
if task['user'] == current_user.nickname or current_user.role_admin():
|
if task['user'] == current_user.nickname or current_user.role_admin():
|
||||||
if task['formStarttime']:
|
if task['formStarttime']:
|
||||||
@@ -638,7 +737,7 @@ def render_task_status(tasklist):
|
|||||||
task['runtime'] = format_runtime(task['formRuntime'])
|
task['runtime'] = format_runtime(task['formRuntime'])
|
||||||
|
|
||||||
# localize the task status
|
# localize the task status
|
||||||
if isinstance( task['stat'], int ):
|
if isinstance(task['stat'], int):
|
||||||
if task['stat'] == STAT_WAITING:
|
if task['stat'] == STAT_WAITING:
|
||||||
task['status'] = _(u'Waiting')
|
task['status'] = _(u'Waiting')
|
||||||
elif task['stat'] == STAT_FAIL:
|
elif task['stat'] == STAT_FAIL:
|
||||||
@@ -651,7 +750,7 @@ def render_task_status(tasklist):
|
|||||||
task['status'] = _(u'Unknown Status')
|
task['status'] = _(u'Unknown Status')
|
||||||
|
|
||||||
# localize the task type
|
# localize the task type
|
||||||
if isinstance( task['taskType'], int ):
|
if isinstance(task['taskType'], int):
|
||||||
if task['taskType'] == TASK_EMAIL:
|
if task['taskType'] == TASK_EMAIL:
|
||||||
task['taskMessage'] = _(u'E-mail: ') + task['taskMess']
|
task['taskMessage'] = _(u'E-mail: ') + task['taskMess']
|
||||||
elif task['taskType'] == TASK_CONVERT:
|
elif task['taskType'] == TASK_CONVERT:
|
||||||
@@ -668,34 +767,14 @@ def render_task_status(tasklist):
|
|||||||
return renderedtasklist
|
return renderedtasklist
|
||||||
|
|
||||||
|
|
||||||
# Language and content filters for displaying in the UI
|
|
||||||
def common_filters():
|
|
||||||
if current_user.filter_language() != "all":
|
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
|
||||||
else:
|
|
||||||
lang_filter = true()
|
|
||||||
content_rating_filter = false() if current_user.mature_content else \
|
|
||||||
db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags()))
|
|
||||||
return and_(lang_filter, ~content_rating_filter)
|
|
||||||
|
|
||||||
def tags_filters():
|
def tags_filters():
|
||||||
return ~(false() if current_user.mature_content else \
|
negtags_list = current_user.list_denied_tags()
|
||||||
db.Tags.name.in_(config.mature_content_tags()))
|
postags_list = current_user.list_allowed_tags()
|
||||||
# return db.session.query(db.Tags).filter(~content_rating_filter).order_by(db.Tags.name).all()
|
neg_content_tags_filter = false() if negtags_list == [''] else db.Tags.name.in_(negtags_list)
|
||||||
|
pos_content_tags_filter = true() if postags_list == [''] else db.Tags.name.in_(postags_list)
|
||||||
|
return and_(pos_content_tags_filter, ~neg_content_tags_filter)
|
||||||
|
|
||||||
|
|
||||||
# Creates for all stored languages a translated speaking name in the array for the UI
|
|
||||||
def speaking_language(languages=None):
|
|
||||||
if not languages:
|
|
||||||
languages = db.session.query(db.Languages).all()
|
|
||||||
for lang in languages:
|
|
||||||
try:
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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/
|
||||||
@@ -710,85 +789,32 @@ def check_valid_domain(domain_text):
|
|||||||
return not len(result)
|
return not len(result)
|
||||||
|
|
||||||
|
|
||||||
# Orders all Authors in the list according to authors sort
|
def get_cc_columns(filter_config_custom_read=False):
|
||||||
def order_authors(entry):
|
tmpcc = calibre_db.session.query(db.Custom_Columns)\
|
||||||
sort_authors = entry.author_sort.split('&')
|
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
authors_ordered = list()
|
|
||||||
error = False
|
|
||||||
for auth in sort_authors:
|
|
||||||
# ToDo: How to handle not found authorname
|
|
||||||
result = db.session.query(db.Authors).filter(db.Authors.sort == auth.lstrip().strip()).first()
|
|
||||||
if not result:
|
|
||||||
error = True
|
|
||||||
break
|
|
||||||
authors_ordered.append(result)
|
|
||||||
if not error:
|
|
||||||
entry.authors = authors_ordered
|
|
||||||
return entry
|
|
||||||
|
|
||||||
|
|
||||||
# Fill indexpage with all requested data from database
|
|
||||||
def fill_indexpage(page, database, db_filter, order, *join):
|
|
||||||
if current_user.show_detail_random():
|
|
||||||
randm = db.session.query(db.Books).filter(common_filters())\
|
|
||||||
.order_by(func.random()).limit(config.config_random_books)
|
|
||||||
else:
|
|
||||||
randm = false()
|
|
||||||
off = int(int(config.config_books_per_page) * (page - 1))
|
|
||||||
pagination = Pagination(page, config.config_books_per_page,
|
|
||||||
len(db.session.query(database).filter(db_filter).filter(common_filters()).all()))
|
|
||||||
entries = db.session.query(database).join(*join, isouter=True).filter(db_filter).filter(common_filters()).\
|
|
||||||
order_by(*order).offset(off).limit(config.config_books_per_page).all()
|
|
||||||
for book in entries:
|
|
||||||
book = order_authors(book)
|
|
||||||
return entries, randm, pagination
|
|
||||||
|
|
||||||
|
|
||||||
def get_typeahead(database, query, replace=('',''), tag_filter=true()):
|
|
||||||
db.session.connection().connection.connection.create_function("lower", 1, lcase)
|
|
||||||
entries = db.session.query(database).filter(tag_filter).filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
|
||||||
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
|
||||||
return json_dumps
|
|
||||||
|
|
||||||
# read search results from calibre-database and return it (function is used for feed and simple search
|
|
||||||
def get_search_results(term):
|
|
||||||
db.session.connection().connection.connection.create_function("lower", 1, lcase)
|
|
||||||
q = list()
|
|
||||||
authorterms = re.split("[, ]+", term)
|
|
||||||
for authorterm in authorterms:
|
|
||||||
q.append(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + authorterm + "%")))
|
|
||||||
|
|
||||||
db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + term + "%"))
|
|
||||||
|
|
||||||
return db.session.query(db.Books).filter(common_filters()).filter(
|
|
||||||
or_(db.Books.tags.any(func.lower(db.Tags.name).ilike("%" + term + "%")),
|
|
||||||
db.Books.series.any(func.lower(db.Series.name).ilike("%" + term + "%")),
|
|
||||||
db.Books.authors.any(and_(*q)),
|
|
||||||
db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + term + "%")),
|
|
||||||
func.lower(db.Books.title).ilike("%" + term + "%")
|
|
||||||
)).all()
|
|
||||||
|
|
||||||
def get_cc_columns():
|
|
||||||
tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
|
||||||
if config.config_columns_to_ignore:
|
|
||||||
cc = []
|
cc = []
|
||||||
for col in tmpcc:
|
r = None
|
||||||
|
if config.config_columns_to_ignore:
|
||||||
r = re.compile(config.config_columns_to_ignore)
|
r = re.compile(config.config_columns_to_ignore)
|
||||||
if r.match(col.label):
|
|
||||||
|
for col in tmpcc:
|
||||||
|
if filter_config_custom_read and config.config_read_column and config.config_read_column == col.id:
|
||||||
|
continue
|
||||||
|
if r and r.match(col.name):
|
||||||
|
continue
|
||||||
cc.append(col)
|
cc.append(col)
|
||||||
else:
|
|
||||||
cc = tmpcc
|
|
||||||
return cc
|
return cc
|
||||||
|
|
||||||
def get_download_link(book_id, book_format):
|
|
||||||
|
def get_download_link(book_id, book_format, client):
|
||||||
book_format = book_format.split(".")[0]
|
book_format = book_format.split(".")[0]
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
|
book = calibre_db.get_filtered_book(book_id)
|
||||||
if book:
|
if book:
|
||||||
data = db.session.query(db.Data).filter(db.Data.book == book.id)\
|
data1 = calibre_db.get_book_format(book.id, book_format.upper())
|
||||||
.filter(db.Data.format == book_format.upper()).first()
|
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
if data:
|
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
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
ub.update_download(book_id, int(current_user.id))
|
ub.update_download(book_id, int(current_user.id))
|
||||||
@@ -798,28 +824,8 @@ def get_download_link(book_id, book_format):
|
|||||||
file_name = get_valid_filename(file_name)
|
file_name = get_valid_filename(file_name)
|
||||||
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*=UTF-8''%s.%s" % (quote(file_name.encode('utf-8')),
|
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
||||||
book_format)
|
quote(file_name.encode('utf-8')), book_format, quote(file_name.encode('utf-8')), book_format)
|
||||||
return do_download_file(book, book_format, data, headers)
|
return do_download_file(book, book_format, client, data1, headers)
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def check_exists_book(authr,title):
|
|
||||||
db.session.connection().connection.connection.create_function("lower", 1, lcase)
|
|
||||||
q = list()
|
|
||||||
authorterms = re.split(r'\s*&\s*', authr)
|
|
||||||
for authorterm in authorterms:
|
|
||||||
q.append(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + authorterm + "%")))
|
|
||||||
|
|
||||||
return db.session.query(db.Books).filter(
|
|
||||||
and_(db.Books.authors.any(and_(*q)),
|
|
||||||
func.lower(db.Books.title).ilike("%" + title + "%")
|
|
||||||
)).first()
|
|
||||||
|
|
||||||
############### Database Helper functions
|
|
||||||
|
|
||||||
def lcase(s):
|
|
||||||
try:
|
|
||||||
return unidecode.unidecode(s.lower())
|
|
||||||
except Exception as e:
|
|
||||||
log.exception(e)
|
|
||||||
|
|||||||
+27
-3
@@ -57,12 +57,36 @@ 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()
|
||||||
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)
|
||||||
language_names.remove(v)
|
language_names.remove(v)
|
||||||
yield k
|
|
||||||
|
|
||||||
if remainder is not None:
|
if remainder is not None:
|
||||||
remainder.extend(language_names)
|
remainder.extend(language_names)
|
||||||
|
return languages
|
||||||
|
|
||||||
|
def get_valid_language_codes(locale, language_names, remainder=None):
|
||||||
|
languages = list()
|
||||||
|
if "" in language_names:
|
||||||
|
language_names.remove("")
|
||||||
|
for k, v in get_language_names(locale).items():
|
||||||
|
if k in language_names:
|
||||||
|
languages.append(k)
|
||||||
|
language_names.remove(k)
|
||||||
|
if remainder is not None and len(language_names):
|
||||||
|
remainder.extend(language_names)
|
||||||
|
return languages
|
||||||
|
|
||||||
|
def get_lang3(lang):
|
||||||
|
try:
|
||||||
|
if len(lang) == 2:
|
||||||
|
ret_value = get(part1=lang).part3
|
||||||
|
elif len(lang) == 3:
|
||||||
|
ret_value = lang
|
||||||
|
else:
|
||||||
|
ret_value = ""
|
||||||
|
except KeyError:
|
||||||
|
ret_value = lang
|
||||||
|
return ret_value
|
||||||
|
|||||||
+378
-3
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# This file is autogenerated, do NOT add, change, or delete ANY string
|
||||||
|
# If you need help or assistance for adding a new language, please contact the project team
|
||||||
|
|
||||||
# map iso639 language codes to language names, translated
|
# map iso639 language codes to language names, translated
|
||||||
|
|
||||||
@@ -852,6 +854,379 @@ LANGUAGE_NAMES = {
|
|||||||
"zxx": "Geen linguïstische inhoud",
|
"zxx": "Geen linguïstische inhoud",
|
||||||
"zza": "Zaza"
|
"zza": "Zaza"
|
||||||
},
|
},
|
||||||
|
"tr": {
|
||||||
|
"abk": "Abhazca",
|
||||||
|
"ace": "Achinese",
|
||||||
|
"ach": "Acoli",
|
||||||
|
"ada": "Adangme",
|
||||||
|
"ady": "Adyghe",
|
||||||
|
"aar": "Afar",
|
||||||
|
"afh": "Afrihili",
|
||||||
|
"afr": "Afrikanca",
|
||||||
|
"ain": "Ainu (Japonca)",
|
||||||
|
"aka": "Akanca (Afrika dili)",
|
||||||
|
"akk": "Akatça",
|
||||||
|
"sqi": "Albanian",
|
||||||
|
"ale": "Alaskaca",
|
||||||
|
"amh": "Etiyopyaca",
|
||||||
|
"anp": "Angika",
|
||||||
|
"ara": "Arapça",
|
||||||
|
"arg": "Aragonca (İspanya)",
|
||||||
|
"arp": "Arapaho (Kuzey Amerika yerlileri)",
|
||||||
|
"arw": "Arawak (Surinam)",
|
||||||
|
"hye": "Ermenice",
|
||||||
|
"asm": "Assamese (Hindistan)",
|
||||||
|
"ast": "Asturyasca",
|
||||||
|
"ava": "Avarca",
|
||||||
|
"ave": "Avestan (Eski İran)",
|
||||||
|
"awa": "Awadhi (Hindistan)",
|
||||||
|
"aym": "Aymara (Güney Amerika)",
|
||||||
|
"aze": "Azerice",
|
||||||
|
"ban": "Balice (Bali adaları)",
|
||||||
|
"bal": "Belucice (İran)",
|
||||||
|
"bam": "Bambara (Mali)",
|
||||||
|
"bas": "Basa (Kamerun)",
|
||||||
|
"bak": "Başkırca",
|
||||||
|
"eus": "Baskça",
|
||||||
|
"bej": "Beja (Eritre; Sudan)",
|
||||||
|
"bel": "Beyaz Rusça",
|
||||||
|
"bem": "Bemba (Zambia)",
|
||||||
|
"ben": "Bengalce",
|
||||||
|
"bho": "Bhojpuri (Hindistan)",
|
||||||
|
"bik": "Bikol (Filipinler)",
|
||||||
|
"byn": "Bilin",
|
||||||
|
"bin": "Bini (Afrika)",
|
||||||
|
"bis": "Bislama (Vanuatu; Kuzey Pasifik)",
|
||||||
|
"zbl": "Blis Sembolleri",
|
||||||
|
"bos": "Boşnakça",
|
||||||
|
"bra": "Braj (Hindistan)",
|
||||||
|
"bre": "Bretonca",
|
||||||
|
"bug": "Buginese (Endonezya)",
|
||||||
|
"bul": "Bulgarca",
|
||||||
|
"bua": "Buriat (Moğolistan)",
|
||||||
|
"mya": "Burmaca",
|
||||||
|
"cad": "Caddo (Kuzey Amerika yerlileri)",
|
||||||
|
"cat": "Katalanca",
|
||||||
|
"ceb": "Cebuano (Filipinler)",
|
||||||
|
"chg": "Çağatayca",
|
||||||
|
"cha": "Chamorro (Guam adaları)",
|
||||||
|
"che": "Çeçence",
|
||||||
|
"chr": "Cherokee (Kuzey Amerika yerlileri)",
|
||||||
|
"chy": "Cheyenne (kuzey Amerika yerlileri)",
|
||||||
|
"chb": "Chibcha (Kolombiya)",
|
||||||
|
"zho": "Çince",
|
||||||
|
"chn": "Chinook lehçesi (Kuzey Batı Amerika kıyıları)",
|
||||||
|
"chp": "Chipewyan (Kuzey Amerika yerlileri)",
|
||||||
|
"cho": "Choctaw (Kuzey Amerika yerlileri)",
|
||||||
|
"chk": "Chuukese",
|
||||||
|
"chv": "Çuvaş (Türkçe)",
|
||||||
|
"cop": "Kıptice (Eski Mısır)",
|
||||||
|
"cor": "Cornish (Kelt)",
|
||||||
|
"cos": "Korsikaca",
|
||||||
|
"cre": "Cree (Kuzey Amerika yerlileri)",
|
||||||
|
"mus": "Creek",
|
||||||
|
"hrv": "Hırvatça",
|
||||||
|
"ces": "Çekçe",
|
||||||
|
"dak": "Dakota (Kuzey Amerika yerlileri)",
|
||||||
|
"dan": "Danimarkaca; Danca",
|
||||||
|
"dar": "Dargwa (Dağıstan)",
|
||||||
|
"del": "Delaware (Kuzey Amerika yerlileri)",
|
||||||
|
"div": "Dhivehi",
|
||||||
|
"din": "Dinka (Sudan)",
|
||||||
|
"doi": "Dogri (makro dili)",
|
||||||
|
"dgr": "Dogrib (Kanada)",
|
||||||
|
"dua": "Duala (Afrika)",
|
||||||
|
"nld": "Flâmanca (Hollanda dili)",
|
||||||
|
"dyu": "Dyula (Burkina Faso; Mali)",
|
||||||
|
"dzo": "Dzongkha (Butan)",
|
||||||
|
"efi": "Efik (Afrika)",
|
||||||
|
"egy": "Mısırca (Eski)",
|
||||||
|
"eka": "Ekajuk (Afrika)",
|
||||||
|
"elx": "Elamca",
|
||||||
|
"eng": "İngilizce",
|
||||||
|
"myv": "Erzya dili",
|
||||||
|
"epo": "Esperanto",
|
||||||
|
"est": "Estonca",
|
||||||
|
"ewe": "Ewe (Afrika)",
|
||||||
|
"ewo": "Ewondo (Afrika)",
|
||||||
|
"fan": "Fang (Ekvatoryal Guinea)",
|
||||||
|
"fat": "Fanti (Afrika)",
|
||||||
|
"fao": "Faroece",
|
||||||
|
"fij": "Fiji dili",
|
||||||
|
"fil": "Filipince",
|
||||||
|
"fin": "Fince",
|
||||||
|
"fon": "Fon (Benin)",
|
||||||
|
"fra": "Fransızca",
|
||||||
|
"fur": "Friulian (İtalya)",
|
||||||
|
"ful": "Fulah (Afrika)",
|
||||||
|
"gaa": "Ganaca",
|
||||||
|
"glg": "Galce",
|
||||||
|
"lug": "Ganda Dili",
|
||||||
|
"gay": "Gayo (Sumatra)",
|
||||||
|
"gba": "Gbaya (Orta Afrika Cumhuriyeti)",
|
||||||
|
"gez": "Geez (Etiyopya)",
|
||||||
|
"kat": "Gürcüce",
|
||||||
|
"deu": "Almanca",
|
||||||
|
"gil": "Kiribati dili",
|
||||||
|
"gon": "Gondi (Hindistan)",
|
||||||
|
"gor": "Gorontalo (Endonezya)",
|
||||||
|
"got": "Gotik",
|
||||||
|
"grb": "Grebo (Liberya)",
|
||||||
|
"grn": "Guarani (Paraguay)",
|
||||||
|
"guj": "Gucaratça",
|
||||||
|
"gwi": "Gwichʼin",
|
||||||
|
"hai": "Haida (Kuzey Amerika yerlileri)",
|
||||||
|
"hau": "Hausa Dili",
|
||||||
|
"haw": "Havai Dili",
|
||||||
|
"heb": "İbranice",
|
||||||
|
"her": "Herero Dili",
|
||||||
|
"hil": "Hiligaynon",
|
||||||
|
"hin": "Hintçe",
|
||||||
|
"hmo": "Hiri Motu",
|
||||||
|
"hit": "Hititçe",
|
||||||
|
"hmn": "Hmong",
|
||||||
|
"hun": "Macarca",
|
||||||
|
"hup": "Hupa",
|
||||||
|
"iba": "Iban",
|
||||||
|
"isl": "İzlandaca",
|
||||||
|
"ido": "Ido Dili",
|
||||||
|
"ibo": "Igbo Dili",
|
||||||
|
"ilo": "Iloko",
|
||||||
|
"ind": "Endonezyaca",
|
||||||
|
"inh": "İnguşca",
|
||||||
|
"ina": "Interlingua (Uluslararası Yardımcı Dil Kurumu)",
|
||||||
|
"ile": "Interlingue",
|
||||||
|
"iku": "Inuktitut",
|
||||||
|
"ipk": "Inupiak Dili",
|
||||||
|
"gle": "İrlandaca",
|
||||||
|
"ita": "İtalyanca",
|
||||||
|
"jpn": "Japonca",
|
||||||
|
"jav": "Cava Dili",
|
||||||
|
"jrb": "Yahudi-Arapçası",
|
||||||
|
"jpr": "Yahudi-Farsça",
|
||||||
|
"kbd": "Kabardian",
|
||||||
|
"kab": "Kabyle",
|
||||||
|
"kac": "Kachin",
|
||||||
|
"kal": "Kalaallisut",
|
||||||
|
"xal": "Kalmyk",
|
||||||
|
"kam": "Kamba (Kenya)",
|
||||||
|
"kan": "Kannada",
|
||||||
|
"kau": "Kanuri Dili",
|
||||||
|
"kaa": "Kara-Kalpak",
|
||||||
|
"krc": "Karachay-Balkar",
|
||||||
|
"krl": "Karelian",
|
||||||
|
"kas": "Keşmirce",
|
||||||
|
"csb": "Kashubian (Lehçe diyalekti)",
|
||||||
|
"kaw": "Kawi",
|
||||||
|
"kaz": "Kazakça",
|
||||||
|
"kha": "Khasi",
|
||||||
|
"kho": "Khotanese",
|
||||||
|
"kik": "Kikuyu Dili",
|
||||||
|
"kmb": "Kimbundu",
|
||||||
|
"kin": "Kinyarwanda",
|
||||||
|
"kir": "Kırgızca",
|
||||||
|
"tlh": "Klingon",
|
||||||
|
"kom": "Komi Dili",
|
||||||
|
"kon": "Kongo Dili",
|
||||||
|
"kok": "Konkani (makro dil)",
|
||||||
|
"kor": "Korece",
|
||||||
|
"kos": "Kosraean",
|
||||||
|
"kpe": "Kpelle",
|
||||||
|
"kua": "Kuanyama Dili",
|
||||||
|
"kum": "Kumyk",
|
||||||
|
"kur": "Kürtçe",
|
||||||
|
"kru": "Kurukh",
|
||||||
|
"kut": "Kutenai",
|
||||||
|
"lad": "Ladino",
|
||||||
|
"lah": "Lahnda",
|
||||||
|
"lam": "Lamba",
|
||||||
|
"lao": "Laos Dili",
|
||||||
|
"lat": "Latince",
|
||||||
|
"lav": "Letonca",
|
||||||
|
"lez": "Lezghian",
|
||||||
|
"lim": "Liburg Dili",
|
||||||
|
"lin": "Lingala Dili",
|
||||||
|
"lit": "Litvanyaca",
|
||||||
|
"jbo": "Lojban dili",
|
||||||
|
"loz": "Lozi",
|
||||||
|
"lub": "Luba Katanga Dili",
|
||||||
|
"lua": "Luba-Lulua",
|
||||||
|
"lui": "Luiseno",
|
||||||
|
"smj": "Lule Sami",
|
||||||
|
"lun": "Lunda",
|
||||||
|
"luo": "Luo (Kenya ve Tanzanya)",
|
||||||
|
"lus": "Lushai",
|
||||||
|
"ltz": "Lüksemburg Dili",
|
||||||
|
"mkd": "Makedonca",
|
||||||
|
"mad": "Madurese",
|
||||||
|
"mag": "Magahi",
|
||||||
|
"mai": "Maithili dili",
|
||||||
|
"mak": "Makasar",
|
||||||
|
"mlg": "Madagaskar Dili",
|
||||||
|
"msa": "Malay (makro dili)",
|
||||||
|
"mal": "Malayalam",
|
||||||
|
"mlt": "Maltaca",
|
||||||
|
"mnc": "Manchu",
|
||||||
|
"mdr": "Mandar",
|
||||||
|
"man": "Mandingo",
|
||||||
|
"mni": "Manipuri dili",
|
||||||
|
"glv": "Manx (Galler)",
|
||||||
|
"mri": "Maori Dili",
|
||||||
|
"arn": "Mapudungun",
|
||||||
|
"mar": "Marathi",
|
||||||
|
"chm": "Mari (Rusya)",
|
||||||
|
"mah": "Marshall Dili",
|
||||||
|
"mwr": "Marwari",
|
||||||
|
"mas": "Masai",
|
||||||
|
"men": "Mende (Sierra Leone)",
|
||||||
|
"mic": "Mi'kmak",
|
||||||
|
"min": "Minangkabau",
|
||||||
|
"mwl": "Mirandese",
|
||||||
|
"moh": "Mohawk",
|
||||||
|
"mdf": "Moşka",
|
||||||
|
"lol": "Mongo",
|
||||||
|
"mon": "Moğol Dili",
|
||||||
|
"mos": "Mossi",
|
||||||
|
"mul": "Çoklu diller",
|
||||||
|
"nqo": "N'Ko",
|
||||||
|
"nau": "Nauru",
|
||||||
|
"nav": "Navajo Dili",
|
||||||
|
"ndo": "Ndonga Dili",
|
||||||
|
"nap": "Neapolitan",
|
||||||
|
"nia": "Nias",
|
||||||
|
"niu": "Niuean",
|
||||||
|
"zxx": "Hiçbir dil içeriği yok",
|
||||||
|
"nog": "Nogai",
|
||||||
|
"nor": "Norveçce",
|
||||||
|
"nob": "Norveççe Bokmal",
|
||||||
|
"nno": "Norveççe Nynorsk",
|
||||||
|
"nym": "Nyamwezi",
|
||||||
|
"nya": "Nyanja",
|
||||||
|
"nyn": "Nyankole",
|
||||||
|
"nyo": "Nyoro",
|
||||||
|
"nzi": "Nzima",
|
||||||
|
"oci": "Oksitanca (1500 sonrası)",
|
||||||
|
"oji": "Ojibwa Dili",
|
||||||
|
"orm": "Oromo Dili",
|
||||||
|
"osa": "Osage",
|
||||||
|
"oss": "Osetya Dili",
|
||||||
|
"pal": "Pehlevi",
|
||||||
|
"pau": "Palauan",
|
||||||
|
"pli": "Pali Dili",
|
||||||
|
"pam": "Pampanga",
|
||||||
|
"pag": "Pangasinan",
|
||||||
|
"pan": "Pencabi Dili",
|
||||||
|
"pap": "Papiamento",
|
||||||
|
"fas": "Farsça",
|
||||||
|
"phn": "Fenikçe",
|
||||||
|
"pon": "Pohnpeian",
|
||||||
|
"pol": "Polonyaca",
|
||||||
|
"por": "Portekizce",
|
||||||
|
"pus": "Pushto",
|
||||||
|
"que": "Quechua",
|
||||||
|
"raj": "Rajasthani",
|
||||||
|
"rap": "Rapanui",
|
||||||
|
"ron": "Rumence",
|
||||||
|
"roh": "Romanca",
|
||||||
|
"rom": "Çingene Dili",
|
||||||
|
"run": "Kirundi",
|
||||||
|
"rus": "Rusça",
|
||||||
|
"smo": "Samoa Dili",
|
||||||
|
"sad": "Sandawe",
|
||||||
|
"sag": "Sangho",
|
||||||
|
"san": "Sanskritçe",
|
||||||
|
"sat": "Santali dili",
|
||||||
|
"srd": "Sardinya",
|
||||||
|
"sas": "Sasak",
|
||||||
|
"sco": "İskoç lehçesi",
|
||||||
|
"sel": "Selkup",
|
||||||
|
"srp": "Sırpça",
|
||||||
|
"srr": "Serer",
|
||||||
|
"shn": "Shan",
|
||||||
|
"sna": "Shona",
|
||||||
|
"scn": "Sicilyalı",
|
||||||
|
"sid": "Sidamo",
|
||||||
|
"bla": "Siksika (Kuzey Amerika yerlileri)",
|
||||||
|
"snd": "Sindhi",
|
||||||
|
"sin": "Sinhala Dili",
|
||||||
|
"den": "Slave (Athapascan; Kuzey Amerika yerlileri)",
|
||||||
|
"slk": "Slovakça",
|
||||||
|
"slv": "Slovence",
|
||||||
|
"sog": "Sogdian",
|
||||||
|
"som": "Somali Dili",
|
||||||
|
"snk": "Soninke",
|
||||||
|
"spa": "İspanyolca",
|
||||||
|
"srn": "Sranan Tongo",
|
||||||
|
"suk": "Sukuma",
|
||||||
|
"sux": "Sümerce",
|
||||||
|
"sun": "Sudan Dili",
|
||||||
|
"sus": "Susu",
|
||||||
|
"swa": "Swahili (makro dil)",
|
||||||
|
"ssw": "Siswati",
|
||||||
|
"swe": "İsveçce",
|
||||||
|
"syr": "Süryanice",
|
||||||
|
"tgl": "Tagalog",
|
||||||
|
"tah": "Tahitice",
|
||||||
|
"tgk": "Tacikçe",
|
||||||
|
"tmh": "Tamashek",
|
||||||
|
"tam": "Tamilce",
|
||||||
|
"tat": "Tatarca",
|
||||||
|
"tel": "Telugu",
|
||||||
|
"ter": "Tereno",
|
||||||
|
"tet": "Tetum",
|
||||||
|
"tha": "Taylandça",
|
||||||
|
"bod": "Tibetçe",
|
||||||
|
"tig": "Tigre",
|
||||||
|
"tir": "Tigrinya Dili",
|
||||||
|
"tem": "Timne",
|
||||||
|
"tiv": "Tiv",
|
||||||
|
"tli": "Tlingit",
|
||||||
|
"tpi": "Tok Pisin",
|
||||||
|
"tkl": "Tokelau",
|
||||||
|
"tog": "Tonga (Nyasa)",
|
||||||
|
"ton": "Tonga",
|
||||||
|
"tsi": "Tsimshian",
|
||||||
|
"tso": "Tsonga",
|
||||||
|
"tsn": "Setswana",
|
||||||
|
"tum": "Tumbuka",
|
||||||
|
"tur": "Türkçe",
|
||||||
|
"tuk": "Türkmence",
|
||||||
|
"tvl": "Tuvalu",
|
||||||
|
"tyv": "Tuvinian",
|
||||||
|
"twi": "Twi",
|
||||||
|
"udm": "Udmurt",
|
||||||
|
"uga": "Ugarit Çivi Yazısı",
|
||||||
|
"uig": "Uygurca",
|
||||||
|
"ukr": "Ukraynaca",
|
||||||
|
"umb": "Umbundu",
|
||||||
|
"mis": "Şifresiz diller",
|
||||||
|
"und": "Belirlenemeyen",
|
||||||
|
"urd": "Urduca",
|
||||||
|
"uzb": "Özbekçe",
|
||||||
|
"vai": "Vai",
|
||||||
|
"ven": "Venda Dili",
|
||||||
|
"vie": "Vietnamca",
|
||||||
|
"vol": "Volapük",
|
||||||
|
"vot": "Votic",
|
||||||
|
"wln": "Valonca",
|
||||||
|
"war": "Waray (Filipinler)",
|
||||||
|
"was": "Vasho",
|
||||||
|
"cym": "Gal Dili",
|
||||||
|
"wal": "Wolaytta",
|
||||||
|
"wol": "Wolof",
|
||||||
|
"xho": "Xhosa",
|
||||||
|
"sah": "Yakut",
|
||||||
|
"yao": "Yao",
|
||||||
|
"yap": "Yapese",
|
||||||
|
"yid": "Yidiş",
|
||||||
|
"yor": "Yoruba",
|
||||||
|
"zap": "Zapotec",
|
||||||
|
"zza": "Zaza",
|
||||||
|
"zen": "Zenaga",
|
||||||
|
"zha": "Zuang Dili",
|
||||||
|
"zul": "Zulu",
|
||||||
|
"zun": "Zuni"
|
||||||
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"aar": "afar",
|
"aar": "afar",
|
||||||
"abk": "abkhaze",
|
"abk": "abkhaze",
|
||||||
@@ -4935,7 +5310,7 @@ LANGUAGE_NAMES = {
|
|||||||
"pon": "Pohnpeian",
|
"pon": "Pohnpeian",
|
||||||
"por": "Portuguese",
|
"por": "Portuguese",
|
||||||
"pro": "Provençal; Old (to 1500)",
|
"pro": "Provençal; Old (to 1500)",
|
||||||
"pus": "Pushto",
|
"pus": "Pashto",
|
||||||
"que": "Quechua",
|
"que": "Quechua",
|
||||||
"raj": "Rajasthani",
|
"raj": "Rajasthani",
|
||||||
"rap": "Rapanui",
|
"rap": "Rapanui",
|
||||||
@@ -6195,7 +6570,7 @@ LANGUAGE_NAMES = {
|
|||||||
"pon": "Pohnpeian",
|
"pon": "Pohnpeian",
|
||||||
"por": "Portuguese",
|
"por": "Portuguese",
|
||||||
"pro": "Provençal; Old (to 1500)",
|
"pro": "Provençal; Old (to 1500)",
|
||||||
"pus": "Pushto",
|
"pus": "Pashto",
|
||||||
"que": "Quechua",
|
"que": "Quechua",
|
||||||
"raj": "Rajasthani",
|
"raj": "Rajasthani",
|
||||||
"rap": "Rapanui",
|
"rap": "Rapanui",
|
||||||
@@ -6615,7 +6990,7 @@ LANGUAGE_NAMES = {
|
|||||||
"pon": "Pohnpeian",
|
"pon": "Pohnpeian",
|
||||||
"por": "Portuguese",
|
"por": "Portuguese",
|
||||||
"pro": "Provençal; Old (to 1500)",
|
"pro": "Provençal; Old (to 1500)",
|
||||||
"pus": "Pushto",
|
"pus": "Pashto",
|
||||||
"que": "Quechua",
|
"que": "Quechua",
|
||||||
"raj": "Rajasthani",
|
"raj": "Rajasthani",
|
||||||
"rap": "Rapanui",
|
"rap": "Rapanui",
|
||||||
|
|||||||
+11
-8
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -81,9 +80,13 @@ def formatdate_filter(val):
|
|||||||
formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
|
formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
|
||||||
return format_date(formatdate, format='medium', locale=get_locale())
|
return format_date(formatdate, format='medium', locale=get_locale())
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, current_user.locale, current_user.nickname)
|
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
|
||||||
|
current_user.locale,
|
||||||
|
current_user.nickname
|
||||||
|
)
|
||||||
return formatdate
|
return formatdate
|
||||||
|
|
||||||
|
|
||||||
@jinjia.app_template_filter('formatdateinput')
|
@jinjia.app_template_filter('formatdateinput')
|
||||||
def format_date_input(val):
|
def format_date_input(val):
|
||||||
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
|
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
|
||||||
@@ -109,9 +112,9 @@ def timestamptodate(date, fmt=None):
|
|||||||
def yesno(value, yes, no):
|
def yesno(value, yes, no):
|
||||||
return yes if value else no
|
return yes if value else no
|
||||||
|
|
||||||
|
@jinjia.app_template_filter('formatfloat')
|
||||||
'''@jinjia.app_template_filter('canread')
|
def formatfloat(value, decimals=1):
|
||||||
def canread(ext):
|
formatedstring = '%d' % value
|
||||||
if isinstance(ext, db.Data):
|
if (value % 1) != 0:
|
||||||
ext = ext.format
|
formatedstring = ('%s.%d' % (formatedstring, (value % 1) * 10**decimals)).rstrip('0')
|
||||||
return ext.lower() in EXTENSIONS_READER'''
|
return formatedstring
|
||||||
|
|||||||
+1101
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2018-2019 shavitmichael, 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
"""This module is used to control authentication/authorization of Kobo sync requests.
|
||||||
|
This module also includes research notes into the auth protocol used by Kobo devices.
|
||||||
|
|
||||||
|
Log-in:
|
||||||
|
When first booting a Kobo device the user must sign into a Kobo (or affiliate) account.
|
||||||
|
Upon successful sign-in, the user is redirected to
|
||||||
|
https://auth.kobobooks.com/CrossDomainSignIn?id=<some id>
|
||||||
|
which serves the following response:
|
||||||
|
<script type='text/javascript'>
|
||||||
|
location.href='kobo://UserAuthenticated?userId=<redacted>&userKey<redacted>&email=<redacted>&returnUrl=https%3a%2f%2fwww.kobo.com';
|
||||||
|
</script>
|
||||||
|
And triggers the insertion of a userKey into the device's User table.
|
||||||
|
|
||||||
|
Together, the device's DeviceId and UserKey act as an *irrevocable* authentication
|
||||||
|
token to most (if not all) Kobo APIs. In fact, in most cases only the UserKey is
|
||||||
|
required to authorize the API call.
|
||||||
|
|
||||||
|
Changing Kobo password *does not* invalidate user keys! This is apparently a known
|
||||||
|
issue for a few years now https://www.mobileread.com/forums/showpost.php?p=3476851&postcount=13
|
||||||
|
(although this poster hypothesised that Kobo could blacklist a DeviceId, many endpoints
|
||||||
|
will still grant access given the userkey.)
|
||||||
|
|
||||||
|
Official Kobo Store Api authorization:
|
||||||
|
* For most of the endpoints we care about (sync, metadata, tags, etc), the userKey is
|
||||||
|
passed in the x-kobo-userkey header, and is sufficient to authorize the API call.
|
||||||
|
* Some endpoints (e.g: AnnotationService) instead make use of Bearer tokens pass through
|
||||||
|
an authorization header. To get a BearerToken, the device makes a POST request to the
|
||||||
|
v1/auth/device endpoint with the secret UserKey and the device's DeviceId.
|
||||||
|
* The book download endpoint passes an auth token as a URL param instead of a header.
|
||||||
|
|
||||||
|
Our implementation:
|
||||||
|
We pretty much ignore all of the above. To authenticate the user, we generate a random
|
||||||
|
and unique token that they append to the CalibreWeb Url when setting up the api_store
|
||||||
|
setting on the device.
|
||||||
|
Thus, every request from the device to the api_store will hit CalibreWeb with the
|
||||||
|
auth_token in the url (e.g: https://mylibrary.com/<auth_token>/v1/library/sync).
|
||||||
|
In addition, once authenticated we also set the login cookie on the response that will
|
||||||
|
be sent back for the duration of the session to authorize subsequent API calls (in
|
||||||
|
particular calls to non-Kobo specific endpoints such as the CalibreWeb book download).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from binascii import hexlify
|
||||||
|
from datetime import datetime
|
||||||
|
from os import urandom
|
||||||
|
|
||||||
|
from flask import g, Blueprint, url_for, abort, request
|
||||||
|
from flask_login import login_user, login_required
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
from . import logger, ub, lm
|
||||||
|
from .web import render_title_template
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import wraps
|
||||||
|
except ImportError:
|
||||||
|
pass # We're not using Python 3
|
||||||
|
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def register_url_value_preprocessor(kobo):
|
||||||
|
@kobo.url_value_preprocessor
|
||||||
|
def pop_auth_token(__, values):
|
||||||
|
g.auth_token = values.pop("auth_token")
|
||||||
|
|
||||||
|
|
||||||
|
def disable_failed_auth_redirect_for_blueprint(bp):
|
||||||
|
lm.blueprint_login_views[bp.name] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_token():
|
||||||
|
if "auth_token" in g:
|
||||||
|
return g.get("auth_token")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def requires_kobo_auth(f):
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
auth_token = get_auth_token()
|
||||||
|
if auth_token is not None:
|
||||||
|
user = (
|
||||||
|
ub.session.query(ub.User)
|
||||||
|
.join(ub.RemoteAuthToken)
|
||||||
|
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if user is not None:
|
||||||
|
login_user(user)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
log.debug("Received Kobo request without a recognizable auth token.")
|
||||||
|
return abort(401)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
||||||
|
|
||||||
|
|
||||||
|
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||||
|
@login_required
|
||||||
|
def generate_auth_token(user_id):
|
||||||
|
host_list = request.host.rsplit(':')
|
||||||
|
if len(host_list) == 1:
|
||||||
|
host = ':'.join(host_list)
|
||||||
|
else:
|
||||||
|
host = ':'.join(host_list[0:-1])
|
||||||
|
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'):
|
||||||
|
warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device')
|
||||||
|
return render_title_template(
|
||||||
|
"generate_kobo_auth_url.html",
|
||||||
|
title=_(u"Kobo Setup"),
|
||||||
|
warning = warning
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||||
|
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
|
||||||
|
ub.RemoteAuthToken.user_id == user_id
|
||||||
|
).filter(ub.RemoteAuthToken.token_type==1).first()
|
||||||
|
|
||||||
|
if not auth_token:
|
||||||
|
auth_token = ub.RemoteAuthToken()
|
||||||
|
auth_token.user_id = user_id
|
||||||
|
auth_token.expiration = datetime.max
|
||||||
|
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
|
||||||
|
auth_token.token_type = 1
|
||||||
|
|
||||||
|
ub.session.add(auth_token)
|
||||||
|
ub.session.commit()
|
||||||
|
return render_title_template(
|
||||||
|
"generate_kobo_auth_url.html",
|
||||||
|
title=_(u"Kobo Setup"),
|
||||||
|
kobo_auth_url=url_for(
|
||||||
|
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
|
||||||
|
),
|
||||||
|
warning = False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@kobo_auth.route("/deleteauthtoken/<int:user_id>")
|
||||||
|
@login_required
|
||||||
|
def delete_auth_token(user_id):
|
||||||
|
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||||
|
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||||
|
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||||
|
ub.session.commit()
|
||||||
|
return ""
|
||||||
+19
-7
@@ -67,6 +67,8 @@ def get_level_name(level):
|
|||||||
|
|
||||||
|
|
||||||
def is_valid_logfile(file_path):
|
def is_valid_logfile(file_path):
|
||||||
|
if file_path == LOG_TO_STDERR or file_path == LOG_TO_STDOUT:
|
||||||
|
return True
|
||||||
if not file_path:
|
if not file_path:
|
||||||
return True
|
return True
|
||||||
if os.path.isdir(file_path):
|
if os.path.isdir(file_path):
|
||||||
@@ -80,7 +82,6 @@ def _absolute_log_file(log_file, default_log_file):
|
|||||||
if not os.path.dirname(log_file):
|
if not os.path.dirname(log_file):
|
||||||
log_file = os.path.join(_CONFIG_DIR, log_file)
|
log_file = os.path.join(_CONFIG_DIR, log_file)
|
||||||
return os.path.abspath(log_file)
|
return os.path.abspath(log_file)
|
||||||
|
|
||||||
return default_log_file
|
return default_log_file
|
||||||
|
|
||||||
|
|
||||||
@@ -105,13 +106,15 @@ def setup(log_file, log_level=None):
|
|||||||
# avoid spamming the log with debug messages from libraries
|
# avoid spamming the log with debug messages from libraries
|
||||||
r.setLevel(log_level)
|
r.setLevel(log_level)
|
||||||
|
|
||||||
|
# Otherwise name get's destroyed on windows
|
||||||
|
if log_file != LOG_TO_STDERR and log_file != LOG_TO_STDOUT:
|
||||||
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
|
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
|
||||||
|
|
||||||
previous_handler = r.handlers[0] if r.handlers else None
|
previous_handler = r.handlers[0] if r.handlers else None
|
||||||
if previous_handler:
|
if previous_handler:
|
||||||
# if the log_file has not changed, don't create a new handler
|
# if the log_file has not changed, don't create a new handler
|
||||||
if getattr(previous_handler, 'baseFilename', None) == log_file:
|
if getattr(previous_handler, 'baseFilename', None) == log_file:
|
||||||
return
|
return "" if log_file == DEFAULT_LOG_FILE else log_file
|
||||||
logging.debug("logging to %s level %s", log_file, r.level)
|
logging.debug("logging to %s level %s", log_file, r.level)
|
||||||
|
|
||||||
if log_file == LOG_TO_STDERR or log_file == LOG_TO_STDOUT:
|
if log_file == LOG_TO_STDERR or log_file == LOG_TO_STDOUT:
|
||||||
@@ -119,21 +122,23 @@ def setup(log_file, log_level=None):
|
|||||||
file_handler = StreamHandler(sys.stdout)
|
file_handler = StreamHandler(sys.stdout)
|
||||||
file_handler.baseFilename = log_file
|
file_handler.baseFilename = log_file
|
||||||
else:
|
else:
|
||||||
file_handler = StreamHandler()
|
file_handler = StreamHandler(sys.stderr)
|
||||||
file_handler.baseFilename = log_file
|
file_handler.baseFilename = log_file
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2)
|
file_handler = RotatingFileHandler(log_file, maxBytes=50000, 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)
|
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2, encoding='utf-8')
|
||||||
|
log_file = ""
|
||||||
file_handler.setFormatter(FORMATTER)
|
file_handler.setFormatter(FORMATTER)
|
||||||
|
|
||||||
for h in r.handlers:
|
for h in r.handlers:
|
||||||
r.removeHandler(h)
|
r.removeHandler(h)
|
||||||
h.close()
|
h.close()
|
||||||
r.addHandler(file_handler)
|
r.addHandler(file_handler)
|
||||||
|
return "" if log_file == DEFAULT_LOG_FILE else log_file
|
||||||
|
|
||||||
|
|
||||||
def create_access_log(log_file, log_name, formatter):
|
def create_access_log(log_file, log_name, formatter):
|
||||||
@@ -146,11 +151,18 @@ def create_access_log(log_file, log_name, formatter):
|
|||||||
access_log = logging.getLogger(log_name)
|
access_log = logging.getLogger(log_name)
|
||||||
access_log.propagate = False
|
access_log.propagate = False
|
||||||
access_log.setLevel(logging.INFO)
|
access_log.setLevel(logging.INFO)
|
||||||
|
try:
|
||||||
|
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
|
||||||
|
except IOError:
|
||||||
|
if log_file == DEFAULT_ACCESS_LOG:
|
||||||
|
raise
|
||||||
|
file_handler = RotatingFileHandler(DEFAULT_ACCESS_LOG, maxBytes=50000, backupCount=2, encoding='utf-8')
|
||||||
|
log_file = ""
|
||||||
|
|
||||||
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2)
|
|
||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
access_log.addHandler(file_handler)
|
access_log.addHandler(file_handler)
|
||||||
return access_log
|
return access_log, \
|
||||||
|
"" if _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) == DEFAULT_ACCESS_LOG else log_file
|
||||||
|
|
||||||
|
|
||||||
# Enable logging of smtp lib debug output
|
# Enable logging of smtp lib debug output
|
||||||
|
|||||||
+15
-5
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -24,13 +23,24 @@ 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
|
||||||
|
backend_resultcode = False # prevent storing values with this resultcode
|
||||||
|
except ImportError:
|
||||||
|
# fails on flask-dance >1.3, due to renaming
|
||||||
|
try:
|
||||||
|
from flask_dance.consumer.storage.sqla import SQLAlchemyStorage as SQLAlchemyBackend
|
||||||
|
from flask_dance.consumer.storage.sqla import first, _get_real_user
|
||||||
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
backend_resultcode = True # prevent storing values with this resultcode
|
||||||
|
except ImportError:
|
||||||
|
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
|
||||||
the `SQLAlchemy`_ ORM.
|
the `SQLAlchemy`_ ORM.
|
||||||
|
|
||||||
.. _SQLAlchemy: http://www.sqlalchemy.org/
|
.. _SQLAlchemy: https://www.sqlalchemy.org/
|
||||||
"""
|
"""
|
||||||
def __init__(self, model, session, provider_id,
|
def __init__(self, model, session, provider_id,
|
||||||
user=None, user_id=None, user_required=None, anon_user=None,
|
user=None, user_id=None, user_required=None, anon_user=None,
|
||||||
@@ -40,7 +50,7 @@ try:
|
|||||||
|
|
||||||
def get(self, blueprint, user=None, user_id=None):
|
def get(self, blueprint, user=None, user_id=None):
|
||||||
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '':
|
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '':
|
||||||
return session[blueprint.name + '_oauth_token']
|
return session[self.provider_id + '_oauth_token']
|
||||||
# check cache
|
# check cache
|
||||||
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
|
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
|
||||||
token = self.cache.get(cache_key)
|
token = self.cache.get(cache_key)
|
||||||
@@ -62,7 +72,7 @@ try:
|
|||||||
use_provider_user_id = True
|
use_provider_user_id = True
|
||||||
|
|
||||||
if self.user_required and not u and not uid and not use_provider_user_id:
|
if self.user_required and not u and not uid and not use_provider_user_id:
|
||||||
#raise ValueError("Cannot get OAuth token without an associated user")
|
# raise ValueError("Cannot get OAuth token without an associated user")
|
||||||
return None
|
return None
|
||||||
# check for user ID
|
# check for user ID
|
||||||
if hasattr(self.model, "user_id") and uid:
|
if hasattr(self.model, "user_id") and uid:
|
||||||
@@ -153,5 +163,5 @@ try:
|
|||||||
blueprint=blueprint, user=user, user_id=user_id,
|
blueprint=blueprint, user=user, user_id=user_id,
|
||||||
))
|
))
|
||||||
|
|
||||||
except ImportError:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
+70
-61
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -36,8 +35,7 @@ from sqlalchemy.orm.exc import NoResultFound
|
|||||||
|
|
||||||
from . import constants, logger, config, app, ub
|
from . import constants, logger, config, app, ub
|
||||||
from .web import login_required
|
from .web import login_required
|
||||||
from .oauth import OAuthBackend
|
from .oauth import OAuthBackend, backend_resultcode
|
||||||
# from .web import github_oauth_required
|
|
||||||
|
|
||||||
|
|
||||||
oauth_check = {}
|
oauth_check = {}
|
||||||
@@ -50,7 +48,7 @@ def oauth_required(f):
|
|||||||
def inner(*args, **kwargs):
|
def inner(*args, **kwargs):
|
||||||
if config.config_login_type == constants.LOGIN_OAUTH:
|
if config.config_login_type == constants.LOGIN_OAUTH:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
if request.is_xhr:
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
data = {'status': 'error', 'message': 'Not Found'}
|
data = {'status': 'error', 'message': 'Not Found'}
|
||||||
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"
|
||||||
@@ -60,29 +58,29 @@ def oauth_required(f):
|
|||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
def register_oauth_blueprint(id, show_name):
|
def register_oauth_blueprint(cid, show_name):
|
||||||
oauth_check[id] = show_name
|
oauth_check[cid] = show_name
|
||||||
|
|
||||||
|
|
||||||
def register_user_with_oauth(user=None):
|
def register_user_with_oauth(user=None):
|
||||||
all_oauth = {}
|
all_oauth = {}
|
||||||
for oauth in oauth_check.keys():
|
for oauth_key in oauth_check.keys():
|
||||||
if str(oauth) + '_oauth_user_id' in session and session[str(oauth) + '_oauth_user_id'] != '':
|
if str(oauth_key) + '_oauth_user_id' in session and session[str(oauth_key) + '_oauth_user_id'] != '':
|
||||||
all_oauth[oauth] = oauth_check[oauth]
|
all_oauth[oauth_key] = oauth_check[oauth_key]
|
||||||
if len(all_oauth.keys()) == 0:
|
if len(all_oauth.keys()) == 0:
|
||||||
return
|
return
|
||||||
if user is None:
|
if user is None:
|
||||||
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
|
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
|
||||||
else:
|
else:
|
||||||
for oauth in all_oauth.keys():
|
for oauth_key in all_oauth.keys():
|
||||||
# Find this OAuth token in the database, or create it
|
# Find this OAuth token in the database, or create it
|
||||||
query = ub.session.query(ub.OAuth).filter_by(
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
provider=oauth,
|
provider=oauth_key,
|
||||||
provider_user_id=session[str(oauth) + "_oauth_user_id"],
|
provider_user_id=session[str(oauth_key) + "_oauth_user_id"],
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
oauth = query.one()
|
oauth_key = query.one()
|
||||||
oauth.user_id = user.id
|
oauth_key.user_id = user.id
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
# no found, return error
|
# no found, return error
|
||||||
return
|
return
|
||||||
@@ -94,39 +92,40 @@ def register_user_with_oauth(user=None):
|
|||||||
|
|
||||||
|
|
||||||
def logout_oauth_user():
|
def logout_oauth_user():
|
||||||
for oauth in oauth_check.keys():
|
for oauth_key in oauth_check.keys():
|
||||||
if str(oauth) + '_oauth_user_id' in session:
|
if str(oauth_key) + '_oauth_user_id' in session:
|
||||||
session.pop(str(oauth) + '_oauth_user_id')
|
session.pop(str(oauth_key) + '_oauth_user_id')
|
||||||
|
|
||||||
|
|
||||||
if ub.oauth_support:
|
if ub.oauth_support:
|
||||||
oauthblueprints =[]
|
oauthblueprints = []
|
||||||
if not ub.session.query(ub.OAuthProvider).count():
|
if not ub.session.query(ub.OAuthProvider).count():
|
||||||
oauth = ub.OAuthProvider()
|
oauthProvider = ub.OAuthProvider()
|
||||||
oauth.provider_name = "github"
|
oauthProvider.provider_name = "github"
|
||||||
oauth.active = False
|
oauthProvider.active = False
|
||||||
ub.session.add(oauth)
|
ub.session.add(oauthProvider)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
oauth = ub.OAuthProvider()
|
oauthProvider = ub.OAuthProvider()
|
||||||
oauth.provider_name = "google"
|
oauthProvider.provider_name = "google"
|
||||||
oauth.active = False
|
oauthProvider.active = False
|
||||||
ub.session.add(oauth)
|
ub.session.add(oauthProvider)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
|
|
||||||
oauth_ids = ub.session.query(ub.OAuthProvider).all()
|
oauth_ids = ub.session.query(ub.OAuthProvider).all()
|
||||||
ele1=dict(provider_name='github',
|
ele1 = dict(provider_name='github',
|
||||||
id=oauth_ids[0].id,
|
id=oauth_ids[0].id,
|
||||||
active=oauth_ids[0].active,
|
active=oauth_ids[0].active,
|
||||||
oauth_client_id=oauth_ids[0].oauth_client_id,
|
oauth_client_id=oauth_ids[0].oauth_client_id,
|
||||||
scope=None,
|
scope=None,
|
||||||
oauth_client_secret=oauth_ids[0].oauth_client_secret,
|
oauth_client_secret=oauth_ids[0].oauth_client_secret,
|
||||||
obtain_link='https://github.com/settings/developers')
|
obtain_link='https://github.com/settings/developers')
|
||||||
ele2=dict(provider_name='google',
|
ele2 = dict(provider_name='google',
|
||||||
id=oauth_ids[1].id,
|
id=oauth_ids[1].id,
|
||||||
active=oauth_ids[1].active,
|
active=oauth_ids[1].active,
|
||||||
scope=["https://www.googleapis.com/auth/plus.me", "https://www.googleapis.com/auth/userinfo.email"],
|
scope=["https://www.googleapis.com/auth/userinfo.email"],
|
||||||
oauth_client_id=oauth_ids[1].oauth_client_id,
|
oauth_client_id=oauth_ids[1].oauth_client_id,
|
||||||
oauth_client_secret=oauth_ids[1].oauth_client_secret,
|
oauth_client_secret=oauth_ids[1].oauth_client_secret,
|
||||||
obtain_link='https://github.com/settings/developers')
|
obtain_link='https://console.developers.google.com/apis/credentials')
|
||||||
oauthblueprints.append(ele1)
|
oauthblueprints.append(ele1)
|
||||||
oauthblueprints.append(ele2)
|
oauthblueprints.append(ele2)
|
||||||
|
|
||||||
@@ -139,12 +138,12 @@ if ub.oauth_support:
|
|||||||
client_id=element['oauth_client_id'],
|
client_id=element['oauth_client_id'],
|
||||||
client_secret=element['oauth_client_secret'],
|
client_secret=element['oauth_client_secret'],
|
||||||
redirect_to="oauth."+element['provider_name']+"_login",
|
redirect_to="oauth."+element['provider_name']+"_login",
|
||||||
scope = element['scope']
|
scope=element['scope']
|
||||||
)
|
)
|
||||||
element['blueprint']=blueprint
|
element['blueprint'] = blueprint
|
||||||
app.register_blueprint(blueprint, url_prefix="/login")
|
|
||||||
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
|
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
|
||||||
user=current_user, user_required=True)
|
user=current_user, user_required=True)
|
||||||
|
app.register_blueprint(blueprint, url_prefix="/login")
|
||||||
if element['active']:
|
if element['active']:
|
||||||
register_oauth_blueprint(element['id'], element['provider_name'])
|
register_oauth_blueprint(element['id'], element['provider_name'])
|
||||||
|
|
||||||
@@ -191,54 +190,64 @@ if ub.oauth_support:
|
|||||||
provider_user_id=provider_user_id,
|
provider_user_id=provider_user_id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
oauth = query.one()
|
oauth_entry = query.one()
|
||||||
# update token
|
# update token
|
||||||
oauth.token = token
|
oauth_entry.token = token
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
oauth = ub.OAuth(
|
oauth_entry = ub.OAuth(
|
||||||
provider=provider_id,
|
provider=provider_id,
|
||||||
provider_user_id=provider_user_id,
|
provider_user_id=provider_user_id,
|
||||||
token=token,
|
token=token,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
ub.session.add(oauth)
|
ub.session.add(oauth_entry)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
|
|
||||||
# Disable Flask-Dance's default behavior for saving the OAuth token
|
# Disable Flask-Dance's default behavior for saving the OAuth token
|
||||||
return False
|
# Value differrs depending on flask-dance version
|
||||||
|
return backend_resultcode
|
||||||
|
|
||||||
|
|
||||||
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url):
|
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
|
||||||
query = ub.session.query(ub.OAuth).filter_by(
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
provider=provider_id,
|
provider=provider_id,
|
||||||
provider_user_id=provider_user_id,
|
provider_user_id=provider_user_id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
oauth = query.one()
|
oauth_entry = query.first()
|
||||||
# already bind with user, just login
|
# already bind with user, just login
|
||||||
if oauth.user:
|
if oauth_entry.user:
|
||||||
login_user(oauth.user)
|
login_user(oauth_entry.user)
|
||||||
|
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname)
|
||||||
|
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname),
|
||||||
|
category="success")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
else:
|
else:
|
||||||
# bind to current user
|
# bind to current user
|
||||||
if current_user and current_user.is_authenticated:
|
if current_user and current_user.is_authenticated:
|
||||||
oauth.user = current_user
|
oauth_entry.user = current_user
|
||||||
try:
|
try:
|
||||||
ub.session.add(oauth)
|
ub.session.add(oauth_entry)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
|
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
||||||
|
return redirect(url_for('web.profile'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
|
else:
|
||||||
|
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
||||||
|
log.info('Login failed, No User Linked With OAuth Account')
|
||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
#if config.config_public_reg:
|
# return redirect(url_for('web.login'))
|
||||||
|
# if config.config_public_reg:
|
||||||
# return redirect(url_for('web.register'))
|
# return redirect(url_for('web.register'))
|
||||||
#else:
|
# else:
|
||||||
# flash(_(u"Public registration is not enabled"), category="error")
|
# flash(_(u"Public registration is not enabled"), category="error")
|
||||||
# return redirect(url_for(redirect_url))
|
# return redirect(url_for(redirect_url))
|
||||||
except NoResultFound:
|
except (NoResultFound, AttributeError):
|
||||||
return redirect(url_for(redirect_url))
|
return redirect(url_for(redirect_url))
|
||||||
|
|
||||||
|
|
||||||
@@ -249,8 +258,8 @@ if ub.oauth_support:
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
oauths = query.all()
|
oauths = query.all()
|
||||||
for oauth in oauths:
|
for oauth_entry in oauths:
|
||||||
status.append(int(oauth.provider))
|
status.append(int(oauth_entry.provider))
|
||||||
return status
|
return status
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
return None
|
return None
|
||||||
@@ -264,21 +273,21 @@ if ub.oauth_support:
|
|||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
oauth = query.one()
|
oauth_entry = query.one()
|
||||||
if current_user and current_user.is_authenticated:
|
if current_user and current_user.is_authenticated:
|
||||||
oauth.user = current_user
|
oauth_entry.user = current_user
|
||||||
try:
|
try:
|
||||||
ub.session.delete(oauth)
|
ub.session.delete(oauth_entry)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
logout_oauth_user()
|
logout_oauth_user()
|
||||||
flash(_(u"Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success")
|
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
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:
|
||||||
log.warning("oauth %s for user %d not fount", provider, current_user.id)
|
log.warning("oauth %s for user %d not found", provider, current_user.id)
|
||||||
flash(_(u"Not linked to %(oauth)s.", oauth=oauth_check[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'))
|
||||||
|
|
||||||
|
|
||||||
@@ -297,7 +306,7 @@ if ub.oauth_support:
|
|||||||
flash(msg, category="error")
|
flash(msg, category="error")
|
||||||
|
|
||||||
|
|
||||||
@oauth.route('/github')
|
@oauth.route('/link/github')
|
||||||
@oauth_required
|
@oauth_required
|
||||||
def github_login():
|
def github_login():
|
||||||
if not github.authorized:
|
if not github.authorized:
|
||||||
@@ -305,7 +314,7 @@ if ub.oauth_support:
|
|||||||
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')
|
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")
|
||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
@@ -316,7 +325,7 @@ if ub.oauth_support:
|
|||||||
return unlink_oauth(oauthblueprints[0]['id'])
|
return unlink_oauth(oauthblueprints[0]['id'])
|
||||||
|
|
||||||
|
|
||||||
@oauth.route('/login/google')
|
@oauth.route('/link/google')
|
||||||
@oauth_required
|
@oauth_required
|
||||||
def google_login():
|
def google_login():
|
||||||
if not google.authorized:
|
if not google.authorized:
|
||||||
@@ -324,7 +333,7 @@ if ub.oauth_support:
|
|||||||
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')
|
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")
|
||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
@@ -346,4 +355,4 @@ if ub.oauth_support:
|
|||||||
@oauth.route('/unlink/google', methods=["GET"])
|
@oauth.route('/unlink/google', methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def google_login_unlink():
|
def google_login_unlink():
|
||||||
return unlink_oauth(oauthblueprints[1]['blueprint'].name)
|
return unlink_oauth(oauthblueprints[1]['id'])
|
||||||
|
|||||||
+141
-69
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -31,10 +30,10 @@ from flask_login import current_user
|
|||||||
from sqlalchemy.sql.expression import func, text, or_, and_
|
from sqlalchemy.sql.expression import func, text, or_, and_
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from . import constants, logger, config, db, ub, services, get_locale, isoLanguages
|
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
|
||||||
from .helper import fill_indexpage, get_download_link, get_book_cover, speaking_language
|
from .helper import get_download_link, get_book_cover
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .web import common_filters, get_search_results, render_read_books, download_required
|
from .web import render_read_books, download_required
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
@@ -57,6 +56,20 @@ def requires_basic_auth_if_no_ano(f):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
class FeedObject:
|
||||||
|
def __init__(self, rating_id, rating_name):
|
||||||
|
self.rating_id = rating_id
|
||||||
|
self.rating_name = rating_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self.rating_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.rating_name
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/")
|
@opds.route("/opds/")
|
||||||
@opds.route("/opds")
|
@opds.route("/opds")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
@@ -87,7 +100,7 @@ def feed_normal_search():
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_new():
|
def feed_new():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
db.Books, True, [db.Books.timestamp.desc()])
|
db.Books, True, [db.Books.timestamp.desc()])
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
@@ -95,7 +108,7 @@ def feed_new():
|
|||||||
@opds.route("/opds/discover")
|
@opds.route("/opds/discover")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_discover():
|
def feed_discover():
|
||||||
entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\
|
entries = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).order_by(func.random())\
|
||||||
.limit(config.config_books_per_page)
|
.limit(config.config_books_per_page)
|
||||||
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
|
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
@@ -105,8 +118,9 @@ def feed_discover():
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_best_rated():
|
def feed_best_rated():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
db.Books, db.Books.ratings.any(db.Ratings.rating > 9), [db.Books.timestamp.desc()])
|
db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
|
||||||
|
[db.Books.timestamp.desc()])
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -119,16 +133,13 @@ def feed_hot():
|
|||||||
hot_books = all_books.offset(off).limit(config.config_books_per_page)
|
hot_books = all_books.offset(off).limit(config.config_books_per_page)
|
||||||
entries = list()
|
entries = list()
|
||||||
for book in hot_books:
|
for book in hot_books:
|
||||||
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first()
|
downloadBook = calibre_db.get_book(book.Downloads.book_id)
|
||||||
if downloadBook:
|
if downloadBook:
|
||||||
entries.append(
|
entries.append(
|
||||||
db.session.query(db.Books).filter(common_filters())
|
calibre_db.get_filtered_book(book.Downloads.book_id)
|
||||||
.filter(db.Books.id == book.Downloads.book_id).first()
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ub.delete_download(book.Downloads.book_id)
|
ub.delete_download(book.Downloads.book_id)
|
||||||
# ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
|
|
||||||
# ub.session.commit()
|
|
||||||
numBooks = entries.__len__()
|
numBooks = entries.__len__()
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1),
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
config.config_books_per_page, numBooks)
|
config.config_books_per_page, numBooks)
|
||||||
@@ -139,10 +150,13 @@ def feed_hot():
|
|||||||
@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
|
off = request.args.get("offset") or 0
|
||||||
entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\
|
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
|
||||||
.group_by(text('books_authors_link.author')).order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off)
|
.filter(calibre_db.common_filters())\
|
||||||
|
.group_by(text('books_authors_link.author'))\
|
||||||
|
.order_by(db.Authors.sort).limit(config.config_books_per_page)\
|
||||||
|
.offset(off)
|
||||||
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(db.session.query(db.Authors).all()))
|
len(calibre_db.session.query(db.Authors).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)
|
||||||
|
|
||||||
|
|
||||||
@@ -150,8 +164,10 @@ def feed_authorindex():
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_author(book_id):
|
def feed_author(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
db.Books, db.Books.authors.any(db.Authors.id == book_id), [db.Books.timestamp.desc()])
|
db.Books,
|
||||||
|
db.Books.authors.any(db.Authors.id == book_id),
|
||||||
|
[db.Books.timestamp.desc()])
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -159,10 +175,14 @@ def feed_author(book_id):
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_publisherindex():
|
def feed_publisherindex():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries = db.session.query(db.Publishers).join(db.books_publishers_link).join(db.Books).filter(common_filters())\
|
entries = calibre_db.session.query(db.Publishers)\
|
||||||
.group_by(text('books_publishers_link.publisher')).order_by(db.Publishers.sort).limit(config.config_books_per_page).offset(off)
|
.join(db.books_publishers_link)\
|
||||||
|
.join(db.Books).filter(calibre_db.common_filters())\
|
||||||
|
.group_by(text('books_publishers_link.publisher'))\
|
||||||
|
.order_by(db.Publishers.sort)\
|
||||||
|
.limit(config.config_books_per_page).offset(off)
|
||||||
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(db.session.query(db.Publishers).all()))
|
len(calibre_db.session.query(db.Publishers).all()))
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -170,8 +190,9 @@ def feed_publisherindex():
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_publisher(book_id):
|
def feed_publisher(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
db.Books, db.Books.publishers.any(db.Publishers.id == book_id),
|
db.Books,
|
||||||
|
db.Books.publishers.any(db.Publishers.id == book_id),
|
||||||
[db.Books.timestamp.desc()])
|
[db.Books.timestamp.desc()])
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
@@ -180,10 +201,16 @@ def feed_publisher(book_id):
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_categoryindex():
|
def feed_categoryindex():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\
|
entries = calibre_db.session.query(db.Tags)\
|
||||||
.group_by(text('books_tags_link.tag')).order_by(db.Tags.name).offset(off).limit(config.config_books_per_page)
|
.join(db.books_tags_link)\
|
||||||
|
.join(db.Books)\
|
||||||
|
.filter(calibre_db.common_filters())\
|
||||||
|
.group_by(text('books_tags_link.tag'))\
|
||||||
|
.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(db.session.query(db.Tags).all()))
|
len(calibre_db.session.query(db.Tags).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)
|
||||||
|
|
||||||
|
|
||||||
@@ -191,8 +218,10 @@ def feed_categoryindex():
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_category(book_id):
|
def feed_category(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
db.Books, db.Books.tags.any(db.Tags.id == book_id), [db.Books.timestamp.desc()])
|
db.Books,
|
||||||
|
db.Books.tags.any(db.Tags.id == book_id),
|
||||||
|
[db.Books.timestamp.desc()])
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -200,10 +229,15 @@ def feed_category(book_id):
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_seriesindex():
|
def feed_seriesindex():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\
|
entries = calibre_db.session.query(db.Series)\
|
||||||
.group_by(text('books_series_link.series')).order_by(db.Series.sort).offset(off).all()
|
.join(db.books_series_link)\
|
||||||
|
.join(db.Books)\
|
||||||
|
.filter(calibre_db.common_filters())\
|
||||||
|
.group_by(text('books_series_link.series'))\
|
||||||
|
.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(db.session.query(db.Series).all()))
|
len(calibre_db.session.query(db.Series).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)
|
||||||
|
|
||||||
|
|
||||||
@@ -211,45 +245,85 @@ def feed_seriesindex():
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_series(book_id):
|
def feed_series(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
db.Books, db.Books.series.any(db.Series.id == book_id), [db.Books.series_index])
|
db.Books,
|
||||||
|
db.Books.series.any(db.Series.id == book_id),
|
||||||
|
[db.Books.series_index])
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/ratings")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_ratingindex():
|
||||||
|
off = request.args.get("offset") or 0
|
||||||
|
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
|
||||||
|
(db.Ratings.rating / 2).label('name')) \
|
||||||
|
.join(db.books_ratings_link)\
|
||||||
|
.join(db.Books)\
|
||||||
|
.filter(calibre_db.common_filters()) \
|
||||||
|
.group_by(text('books_ratings_link.rating'))\
|
||||||
|
.order_by(db.Ratings.rating).all()
|
||||||
|
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
len(entries))
|
||||||
|
element = list()
|
||||||
|
for entry in entries:
|
||||||
|
element.append(FeedObject(entry[0].id, "{} Stars".format(entry.name)))
|
||||||
|
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/ratings/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_ratings(book_id):
|
||||||
|
off = request.args.get("offset") or 0
|
||||||
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
|
db.Books,
|
||||||
|
db.Books.ratings.any(db.Ratings.id == book_id),
|
||||||
|
[db.Books.timestamp.desc()])
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/formats")
|
@opds.route("/opds/formats")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_formatindex():
|
def feed_formatindex():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries = db.session.query(db.Data).join(db.Books).filter(common_filters()) \
|
entries = calibre_db.session.query(db.Data).join(db.Books)\
|
||||||
.group_by(db.Data.format).order_by(db.Data.format).all()
|
.filter(calibre_db.common_filters()) \
|
||||||
|
.group_by(db.Data.format)\
|
||||||
|
.order_by(db.Data.format).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(entries))
|
len(entries))
|
||||||
|
|
||||||
|
element = list()
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
entry.name = entry.format
|
element.append(FeedObject(entry.format, entry.format))
|
||||||
entry.id = entry.format
|
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination)
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_format', pagination=pagination)
|
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/formats/<book_id>")
|
@opds.route("/opds/formats/<book_id>")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_format(book_id):
|
def feed_format(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
db.Books, db.Books.data.any(db.Data.format == book_id.upper()), [db.Books.timestamp.desc()])
|
db.Books,
|
||||||
|
db.Books.data.any(db.Data.format == book_id.upper()),
|
||||||
|
[db.Books.timestamp.desc()])
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/language")
|
@opds.route("/opds/language")
|
||||||
@opds.route("/opds/language/")
|
@opds.route("/opds/language/")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_languagesindex():
|
def feed_languagesindex():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
if current_user.filter_language() == u"all":
|
if current_user.filter_language() == u"all":
|
||||||
languages = 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 = 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:
|
if cur_l:
|
||||||
languages[0].name = cur_l.get_language_name(get_locale())
|
languages[0].name = cur_l.get_language_name(get_locale())
|
||||||
@@ -264,29 +338,19 @@ def feed_languagesindex():
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_languages(book_id):
|
def feed_languages(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
db.Books, db.Books.languages.any(db.Languages.id == book_id), [db.Books.timestamp.desc()])
|
db.Books,
|
||||||
'''for entry in entries:
|
db.Books.languages.any(db.Languages.id == book_id),
|
||||||
for index in range(0, len(entry.languages)):
|
[db.Books.timestamp.desc()])
|
||||||
try:
|
|
||||||
entry.languages[index].language_name = LC.parse(entry.languages[index].lang_code).get_language_name(
|
|
||||||
get_locale())
|
|
||||||
except UnknownLocaleError:
|
|
||||||
entry.languages[index].language_name = _(
|
|
||||||
isoLanguages.get(part3=entry.languages[index].lang_code).name)'''
|
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
@opds.route("/opds/shelfindex", defaults={'public': 0})
|
|
||||||
@opds.route("/opds/shelfindex/<string:public>")
|
@opds.route("/opds/shelfindex")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_shelfindex(public):
|
def feed_shelfindex():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
if public != 0:
|
shelf = g.shelves_access
|
||||||
shelf = g.public_shelfes
|
|
||||||
number = len(shelf)
|
number = len(shelf)
|
||||||
else:
|
|
||||||
shelf = g.user.shelf
|
|
||||||
number = shelf.count()
|
|
||||||
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,
|
||||||
number)
|
number)
|
||||||
return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination)
|
||||||
@@ -297,7 +361,8 @@ def feed_shelfindex(public):
|
|||||||
def feed_shelf(book_id):
|
def feed_shelf(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
if current_user.is_anonymous:
|
if current_user.is_anonymous:
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1,
|
||||||
|
ub.Shelf.id == book_id).first()
|
||||||
else:
|
else:
|
||||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
||||||
ub.Shelf.id == book_id),
|
ub.Shelf.id == book_id),
|
||||||
@@ -309,7 +374,7 @@ def feed_shelf(book_id):
|
|||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == book_id).order_by(
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == book_id).order_by(
|
||||||
ub.BookShelf.order.asc()).all()
|
ub.BookShelf.order.asc()).all()
|
||||||
for book in books_in_shelf:
|
for book in books_in_shelf:
|
||||||
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
|
cur_book = calibre_db.get_book(book.book_id)
|
||||||
result.append(cur_book)
|
result.append(cur_book)
|
||||||
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(result))
|
len(result))
|
||||||
@@ -320,14 +385,18 @@ def feed_shelf(book_id):
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
@download_required
|
@download_required
|
||||||
def opds_download_link(book_id, book_format):
|
def opds_download_link(book_id, book_format):
|
||||||
return get_download_link(book_id,book_format)
|
if "Kobo" in request.headers.get('User-Agent'):
|
||||||
|
client = "kobo"
|
||||||
|
else:
|
||||||
|
client = ""
|
||||||
|
return get_download_link(book_id, book_format.lower(), client)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/ajax/book/<string:uuid>/<library>")
|
@opds.route("/ajax/book/<string:uuid>/<library>")
|
||||||
@opds.route("/ajax/book/<string:uuid>")
|
@opds.route("/ajax/book/<string:uuid>", defaults={'library': ""})
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def get_metadata_calibre_companion(uuid, library):
|
def get_metadata_calibre_companion(uuid, library):
|
||||||
entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first()
|
entry = calibre_db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first()
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
js = render_template('json.txt', entry=entry)
|
js = render_template('json.txt', entry=entry)
|
||||||
response = make_response(js)
|
response = make_response(js)
|
||||||
@@ -339,17 +408,17 @@ def get_metadata_calibre_companion(uuid, library):
|
|||||||
|
|
||||||
def feed_search(term):
|
def feed_search(term):
|
||||||
if term:
|
if term:
|
||||||
term = term.strip().lower()
|
entries = calibre_db.get_search_results(term)
|
||||||
entries = get_search_results( term)
|
|
||||||
entriescount = len(entries) if len(entries) > 0 else 1
|
entriescount = len(entries) if len(entries) > 0 else 1
|
||||||
pagination = Pagination(1, entriescount, entriescount)
|
pagination = Pagination(1, entriescount, entriescount)
|
||||||
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
|
||||||
else:
|
else:
|
||||||
return render_xml_template('feed.xml', searchterm="")
|
return render_xml_template('feed.xml', searchterm="")
|
||||||
|
|
||||||
|
|
||||||
def check_auth(username, password):
|
def check_auth(username, password):
|
||||||
if sys.version_info.major == 3:
|
if sys.version_info.major == 3:
|
||||||
username=username.encode('windows-1252')
|
username = username.encode('windows-1252')
|
||||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) ==
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) ==
|
||||||
username.decode('utf-8').lower()).first()
|
username.decode('utf-8').lower()).first()
|
||||||
return bool(user and check_password_hash(str(user.password), password))
|
return bool(user and check_password_hash(str(user.password), password))
|
||||||
@@ -363,13 +432,14 @@ def authenticate():
|
|||||||
|
|
||||||
|
|
||||||
def render_xml_template(*args, **kwargs):
|
def render_xml_template(*args, **kwargs):
|
||||||
#ToDo: return time in current timezone similar to %z
|
# ToDo: return time in current timezone similar to %z
|
||||||
currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
||||||
xml = render_template(current_time=currtime, instance=config.config_calibre_web_title, *args, **kwargs)
|
xml = render_template(current_time=currtime, instance=config.config_calibre_web_title, *args, **kwargs)
|
||||||
response = make_response(xml)
|
response = make_response(xml)
|
||||||
response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
|
response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/thumb_240_240/<book_id>")
|
@opds.route("/opds/thumb_240_240/<book_id>")
|
||||||
@opds.route("/opds/cover_240_240/<book_id>")
|
@opds.route("/opds/cover_240_240/<book_id>")
|
||||||
@opds.route("/opds/cover_90_90/<book_id>")
|
@opds.route("/opds/cover_90_90/<book_id>")
|
||||||
@@ -378,6 +448,7 @@ def render_xml_template(*args, **kwargs):
|
|||||||
def feed_get_cover(book_id):
|
def feed_get_cover(book_id):
|
||||||
return get_book_cover(book_id)
|
return get_book_cover(book_id)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/readbooks")
|
@opds.route("/opds/readbooks")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_read_books():
|
def feed_read_books():
|
||||||
@@ -385,6 +456,7 @@ def feed_read_books():
|
|||||||
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
|
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
|
||||||
return render_xml_template('feed.xml', entries=result, pagination=pagination)
|
return render_xml_template('feed.xml', entries=result, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/unreadbooks")
|
@opds.route("/opds/unreadbooks")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_unread_books():
|
def feed_unread_books():
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Flask License
|
# Flask License
|
||||||
|
|||||||
+8
-1
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Flask License
|
# Flask License
|
||||||
@@ -60,10 +59,13 @@ class ReverseProxied(object):
|
|||||||
|
|
||||||
def __init__(self, application):
|
def __init__(self, application):
|
||||||
self.app = application
|
self.app = application
|
||||||
|
self.proxied = False
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
|
self.proxied = False
|
||||||
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
||||||
if script_name:
|
if script_name:
|
||||||
|
self.proxied = True
|
||||||
environ['SCRIPT_NAME'] = script_name
|
environ['SCRIPT_NAME'] = script_name
|
||||||
path_info = environ.get('PATH_INFO', '')
|
path_info = environ.get('PATH_INFO', '')
|
||||||
if path_info and path_info.startswith(script_name):
|
if path_info and path_info.startswith(script_name):
|
||||||
@@ -75,4 +77,9 @@ class ReverseProxied(object):
|
|||||||
servr = environ.get('HTTP_X_FORWARDED_HOST', '')
|
servr = environ.get('HTTP_X_FORWARDED_HOST', '')
|
||||||
if servr:
|
if servr:
|
||||||
environ['HTTP_HOST'] = servr
|
environ['HTTP_HOST'] = servr
|
||||||
|
self.proxied = True
|
||||||
return self.app(environ, start_response)
|
return self.app(environ, start_response)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_proxied(self):
|
||||||
|
return self.proxied
|
||||||
|
|||||||
Executable → Regular
+20
-7
@@ -43,7 +43,6 @@ from . import logger
|
|||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _readable_listen_address(address, port):
|
def _readable_listen_address(address, port):
|
||||||
if ':' in address:
|
if ':' in address:
|
||||||
address = "[" + address + "]"
|
address = "[" + address + "]"
|
||||||
@@ -73,7 +72,11 @@ class WebServer(object):
|
|||||||
if config.config_access_log:
|
if config.config_access_log:
|
||||||
log_name = "gevent.access" if _GEVENT else "tornado.access"
|
log_name = "gevent.access" if _GEVENT else "tornado.access"
|
||||||
formatter = logger.ACCESS_FORMATTER_GEVENT if _GEVENT else logger.ACCESS_FORMATTER_TORNADO
|
formatter = logger.ACCESS_FORMATTER_GEVENT if _GEVENT else logger.ACCESS_FORMATTER_TORNADO
|
||||||
self.access_logger = logger.create_access_log(config.config_access_logfile, log_name, formatter)
|
self.access_logger, logfile = logger.create_access_log(config.config_access_logfile, log_name, formatter)
|
||||||
|
if logfile != config.config_access_logfile:
|
||||||
|
log.warning("Accesslog path %s not valid, falling back to default", config.config_access_logfile)
|
||||||
|
config.config_access_logfile = logfile
|
||||||
|
config.save()
|
||||||
else:
|
else:
|
||||||
if not _GEVENT:
|
if not _GEVENT:
|
||||||
logger.get('tornado.access').disabled = True
|
logger.get('tornado.access').disabled = True
|
||||||
@@ -84,7 +87,8 @@ class WebServer(object):
|
|||||||
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
|
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
|
||||||
self.ssl_args = dict(certfile=certfile_path, keyfile=keyfile_path)
|
self.ssl_args = dict(certfile=certfile_path, keyfile=keyfile_path)
|
||||||
else:
|
else:
|
||||||
log.warning('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl.')
|
log.warning('The specified paths for the ssl certificate file and/or key file seem to be broken. '
|
||||||
|
'Ignoring ssl.')
|
||||||
log.warning('Cert path: %s', certfile_path)
|
log.warning('Cert path: %s', certfile_path)
|
||||||
log.warning('Key path: %s', keyfile_path)
|
log.warning('Key path: %s', keyfile_path)
|
||||||
|
|
||||||
@@ -146,7 +150,7 @@ class WebServer(object):
|
|||||||
self.unix_socket_file = None
|
self.unix_socket_file = None
|
||||||
|
|
||||||
def _start_tornado(self):
|
def _start_tornado(self):
|
||||||
if os.name == 'nt':
|
if os.name == 'nt' and sys.version_info > (3, 7):
|
||||||
import asyncio
|
import asyncio
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port))
|
log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port))
|
||||||
@@ -156,7 +160,7 @@ class WebServer(object):
|
|||||||
max_buffer_size=209700000,
|
max_buffer_size=209700000,
|
||||||
ssl_options=self.ssl_args)
|
ssl_options=self.ssl_args)
|
||||||
http_server.listen(self.listen_port, self.listen_address)
|
http_server.listen(self.listen_port, self.listen_address)
|
||||||
self.wsgiserver = IOLoop.instance()
|
self.wsgiserver = IOLoop.current()
|
||||||
self.wsgiserver.start()
|
self.wsgiserver.start()
|
||||||
# wait for stop signal
|
# wait for stop signal
|
||||||
self.wsgiserver.close(True)
|
self.wsgiserver.close(True)
|
||||||
@@ -171,12 +175,15 @@ class WebServer(object):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error("Error starting server: %s", ex)
|
log.error("Error starting server: %s", ex)
|
||||||
print("Error starting server: %s" % ex)
|
print("Error starting server: %s" % ex)
|
||||||
|
self.stop()
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
self.wsgiserver = None
|
self.wsgiserver = None
|
||||||
|
|
||||||
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")
|
||||||
@@ -187,14 +194,20 @@ class WebServer(object):
|
|||||||
os.execv(sys.executable, arguments)
|
os.execv(sys.executable, arguments)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _killServer(self, ignored_signum, ignored_frame):
|
def _killServer(self, __, ___):
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
def stop(self, restart=False):
|
def stop(self, restart=False):
|
||||||
|
from . import updater_thread
|
||||||
|
updater_thread.stop()
|
||||||
|
from . import calibre_db
|
||||||
|
calibre_db.stop()
|
||||||
|
|
||||||
|
|
||||||
log.info("webserver stop (restart=%s)", restart)
|
log.info("webserver stop (restart=%s)", restart)
|
||||||
self.restart = restart
|
self.restart = restart
|
||||||
if self.wsgiserver:
|
if self.wsgiserver:
|
||||||
if _GEVENT:
|
if _GEVENT:
|
||||||
self.wsgiserver.close()
|
self.wsgiserver.close()
|
||||||
else:
|
else:
|
||||||
self.wsgiserver.add_callback(self.wsgiserver.stop)
|
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2018-2019 shavitmichael, 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 sys
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from jsonschema import validate, exceptions, __version__
|
||||||
|
from datetime import datetime
|
||||||
|
try:
|
||||||
|
from urllib import unquote
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from flask import json
|
||||||
|
from .. import logger
|
||||||
|
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
|
||||||
|
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
|
||||||
|
def to_epoch_timestamp(datetime_object):
|
||||||
|
return (datetime_object - datetime(1970, 1, 1)).total_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
def get_datetime_from_json(json_object, field_name):
|
||||||
|
try:
|
||||||
|
return datetime.utcfromtimestamp(json_object[field_name])
|
||||||
|
except (KeyError, OSError, OverflowError):
|
||||||
|
# OSError is thrown on Windows if timestamp is <1970 or >2038
|
||||||
|
return datetime.min
|
||||||
|
|
||||||
|
|
||||||
|
class SyncToken:
|
||||||
|
""" The SyncToken is used to persist state accross requests.
|
||||||
|
When serialized over the response headers, the Kobo device will propagate the token onto following
|
||||||
|
requests to the service. As an example use-case, the SyncToken is used to detect books that have been added
|
||||||
|
to the library since the last time the device synced to the server.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
books_last_created: Datetime representing the newest 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"
|
||||||
|
VERSION = "1-1-0"
|
||||||
|
LAST_MODIFIED_ADDED_VERSION = "1-1-0"
|
||||||
|
MIN_VERSION = "1-0-0"
|
||||||
|
|
||||||
|
token_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"version": {"type": "string"}, "data": {"type": "object"}, },
|
||||||
|
}
|
||||||
|
# This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
|
||||||
|
# A potential solution might be to keep a list of all known book uuids in the token, and look for any missing
|
||||||
|
# from the db.
|
||||||
|
data_schema_v1 = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"raw_kobo_store_token": {"type": "string"},
|
||||||
|
"books_last_modified": {"type": "string"},
|
||||||
|
"books_last_created": {"type": "string"},
|
||||||
|
"archive_last_modified": {"type": "string"},
|
||||||
|
"reading_state_last_modified": {"type": "string"},
|
||||||
|
"tags_last_modified": {"type": "string"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
raw_kobo_store_token="",
|
||||||
|
books_last_created=datetime.min,
|
||||||
|
books_last_modified=datetime.min,
|
||||||
|
archive_last_modified=datetime.min,
|
||||||
|
reading_state_last_modified=datetime.min,
|
||||||
|
tags_last_modified=datetime.min,
|
||||||
|
):
|
||||||
|
self.raw_kobo_store_token = raw_kobo_store_token
|
||||||
|
self.books_last_created = books_last_created
|
||||||
|
self.books_last_modified = books_last_modified
|
||||||
|
self.archive_last_modified = archive_last_modified
|
||||||
|
self.reading_state_last_modified = reading_state_last_modified
|
||||||
|
self.tags_last_modified = tags_last_modified
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_headers(headers):
|
||||||
|
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
|
||||||
|
if sync_token_header == "":
|
||||||
|
return SyncToken()
|
||||||
|
|
||||||
|
# On the first sync from a Kobo device, we may receive the SyncToken
|
||||||
|
# from the official Kobo store. Without digging too deep into it, that
|
||||||
|
# token is of the form [b64encoded blob].[b64encoded blob 2]
|
||||||
|
if "." in sync_token_header:
|
||||||
|
return SyncToken(raw_kobo_store_token=sync_token_header)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sync_token_json = json.loads(
|
||||||
|
b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4))
|
||||||
|
)
|
||||||
|
validate(sync_token_json, SyncToken.token_schema)
|
||||||
|
if sync_token_json["version"] < SyncToken.MIN_VERSION:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
data_json = sync_token_json["data"]
|
||||||
|
validate(sync_token_json, SyncToken.data_schema_v1)
|
||||||
|
except (exceptions.ValidationError, ValueError):
|
||||||
|
log.error("Sync token contents do not follow the expected json schema.")
|
||||||
|
return SyncToken()
|
||||||
|
|
||||||
|
raw_kobo_store_token = data_json["raw_kobo_store_token"]
|
||||||
|
try:
|
||||||
|
books_last_modified = get_datetime_from_json(data_json, "books_last_modified")
|
||||||
|
books_last_created = get_datetime_from_json(data_json, "books_last_created")
|
||||||
|
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")
|
||||||
|
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
||||||
|
except TypeError:
|
||||||
|
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,
|
||||||
|
books_last_created=books_last_created,
|
||||||
|
books_last_modified=books_last_modified,
|
||||||
|
archive_last_modified=archive_last_modified,
|
||||||
|
reading_state_last_modified=reading_state_last_modified,
|
||||||
|
tags_last_modified=tags_last_modified
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_kobo_store_header(self, store_headers):
|
||||||
|
store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token)
|
||||||
|
|
||||||
|
def merge_from_store_response(self, store_response):
|
||||||
|
self.raw_kobo_store_token = store_response.headers.get(
|
||||||
|
SyncToken.SYNC_TOKEN_HEADER, ""
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_headers(self, headers):
|
||||||
|
headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
|
||||||
|
|
||||||
|
def build_sync_token(self):
|
||||||
|
token = {
|
||||||
|
"version": SyncToken.VERSION,
|
||||||
|
"data": {
|
||||||
|
"raw_kobo_store_token": self.raw_kobo_store_token,
|
||||||
|
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
|
||||||
|
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
||||||
|
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
||||||
|
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
||||||
|
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return b64encode_json(token)
|
||||||
@@ -26,13 +26,22 @@ log = logger.create()
|
|||||||
|
|
||||||
try: from . import goodreads_support
|
try: from . import goodreads_support
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
log.debug("cannot import goodreads, showing authors-metadata will not work: %s", err)
|
log.debug("Cannot import goodreads, showing authors-metadata will not work: %s", err)
|
||||||
goodreads_support = None
|
goodreads_support = None
|
||||||
|
|
||||||
|
|
||||||
try: from . import simpleldap as ldap
|
try:
|
||||||
|
from . import simpleldap as ldap
|
||||||
|
from .simpleldap import ldapVersion
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err)
|
log.debug("Cannot import simpleldap, logging in with ldap will not work: %s", err)
|
||||||
ldap = None
|
ldap = None
|
||||||
|
ldapVersion = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import SyncToken as SyncToken
|
||||||
|
kobo = True
|
||||||
|
except ImportError as err:
|
||||||
|
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
|
||||||
|
kobo = None
|
||||||
|
SyncToken = None
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ from __future__ import division, print_function, unicode_literals
|
|||||||
import time
|
import time
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
from goodreads.client import GoodreadsClient
|
try:
|
||||||
|
from goodreads.client import GoodreadsClient
|
||||||
|
except ImportError:
|
||||||
|
from betterreads.client import GoodreadsClient
|
||||||
|
|
||||||
try: import Levenshtein
|
try: import Levenshtein
|
||||||
except ImportError: Levenshtein = False
|
except ImportError: Levenshtein = False
|
||||||
@@ -95,6 +98,10 @@ def get_other_books(author_info, library_books=None):
|
|||||||
for book in author_info.books:
|
for book in author_info.books:
|
||||||
if book.isbn in identifiers:
|
if book.isbn in identifiers:
|
||||||
continue
|
continue
|
||||||
|
if isinstance(book.gid, int):
|
||||||
|
if book.gid in identifiers:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
if book.gid["#text"] in identifiers:
|
if book.gid["#text"] in identifiers:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
+52
-15
@@ -23,6 +23,10 @@ from flask_simpleldap import LDAP, LDAPException
|
|||||||
|
|
||||||
from .. import constants, logger
|
from .. import constants, logger
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ldap.pkginfo import __version__ as ldapVersion
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
_ldap = LDAP()
|
_ldap = LDAP()
|
||||||
@@ -34,44 +38,77 @@ def init_app(app, config):
|
|||||||
|
|
||||||
app.config['LDAP_HOST'] = config.config_ldap_provider_url
|
app.config['LDAP_HOST'] = config.config_ldap_provider_url
|
||||||
app.config['LDAP_PORT'] = config.config_ldap_port
|
app.config['LDAP_PORT'] = config.config_ldap_port
|
||||||
app.config['LDAP_SCHEMA'] = config.config_ldap_schema
|
if config.config_ldap_encryption == 2:
|
||||||
app.config['LDAP_USERNAME'] = config.config_ldap_user_object.replace('%s', config.config_ldap_serv_username)\
|
app.config['LDAP_SCHEMA'] = 'ldaps'
|
||||||
+ ',' + config.config_ldap_dn
|
else:
|
||||||
|
app.config['LDAP_SCHEMA'] = 'ldap'
|
||||||
|
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
|
||||||
|
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
|
||||||
|
if config.config_ldap_serv_password is None:
|
||||||
|
config.config_ldap_serv_password = ''
|
||||||
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password)
|
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password)
|
||||||
app.config['LDAP_REQUIRE_CERT'] = bool(config.config_ldap_require_cert)
|
else:
|
||||||
if config.config_ldap_require_cert:
|
app.config['LDAP_PASSWORD'] = base64.b64decode("")
|
||||||
|
app.config['LDAP_USERNAME'] = config.config_ldap_serv_username
|
||||||
|
else:
|
||||||
|
app.config['LDAP_USERNAME'] = ""
|
||||||
|
app.config['LDAP_PASSWORD'] = base64.b64decode("")
|
||||||
|
if bool(config.config_ldap_cert_path):
|
||||||
|
app.config['LDAP_REQUIRE_CERT'] = True
|
||||||
app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path
|
app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path
|
||||||
app.config['LDAP_BASE_DN'] = config.config_ldap_dn
|
app.config['LDAP_BASE_DN'] = config.config_ldap_dn
|
||||||
app.config['LDAP_USER_OBJECT_FILTER'] = config.config_ldap_user_object
|
app.config['LDAP_USER_OBJECT_FILTER'] = config.config_ldap_user_object
|
||||||
app.config['LDAP_USE_SSL'] = bool(config.config_ldap_use_ssl)
|
|
||||||
app.config['LDAP_USE_TLS'] = bool(config.config_ldap_use_tls)
|
app.config['LDAP_USE_TLS'] = bool(config.config_ldap_encryption == 1)
|
||||||
|
app.config['LDAP_USE_SSL'] = bool(config.config_ldap_encryption == 2)
|
||||||
app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap)
|
app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap)
|
||||||
|
app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
|
||||||
|
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
|
||||||
|
|
||||||
|
try:
|
||||||
_ldap.init_app(app)
|
_ldap.init_app(app)
|
||||||
|
except RuntimeError as e:
|
||||||
|
log.error(e)
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_details(user=None, group=None, query_filter=None, dn_only=False):
|
||||||
|
return _ldap.get_object_details(user, group, query_filter, dn_only)
|
||||||
|
|
||||||
|
|
||||||
|
def bind():
|
||||||
|
return _ldap.bind()
|
||||||
|
|
||||||
|
|
||||||
|
def get_group_members(group):
|
||||||
|
return _ldap.get_group_members(group)
|
||||||
|
|
||||||
|
|
||||||
def basic_auth_required(func):
|
def basic_auth_required(func):
|
||||||
return _ldap.basic_auth_required(func)
|
return _ldap.basic_auth_required(func)
|
||||||
|
|
||||||
|
|
||||||
def bind_user(username, password):
|
def bind_user(username, password):
|
||||||
# ulf= _ldap.get_object_details('admin')
|
|
||||||
'''Attempts a LDAP login.
|
'''Attempts a LDAP login.
|
||||||
|
|
||||||
:returns: True if login succeeded, False if login failed, None if server unavailable.
|
:returns: True if login succeeded, False if login failed, None if server unavailable.
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
|
if _ldap.get_object_details(username):
|
||||||
result = _ldap.bind_user(username, password)
|
result = _ldap.bind_user(username, password)
|
||||||
log.debug("LDAP login '%s': %r", username, result)
|
log.debug("LDAP login '%s': %r", username, result)
|
||||||
return result is not None
|
return result is not None, None
|
||||||
|
return None, None # User not found
|
||||||
|
except (TypeError, AttributeError, KeyError) as ex:
|
||||||
|
error = ("LDAP bind_user: %s" % ex)
|
||||||
|
return None, error
|
||||||
except LDAPException as ex:
|
except LDAPException as ex:
|
||||||
if ex.message == 'Invalid credentials':
|
if ex.message == 'Invalid credentials':
|
||||||
log.info("LDAP login '%s' failed: %s", username, ex)
|
error = ("LDAP admin login failed")
|
||||||
return False
|
return None, error
|
||||||
if ex.message == "Can't contact LDAP server":
|
if ex.message == "Can't contact LDAP server":
|
||||||
log.warning('LDAP Server down: %s', ex)
|
# log.warning('LDAP Server down: %s', ex)
|
||||||
return None
|
error = ('LDAP Server down: %s' % ex)
|
||||||
|
return None, error
|
||||||
else:
|
else:
|
||||||
log.warning('LDAP Server error: %s', ex.message)
|
error = ('LDAP Server error: %s' % ex.message)
|
||||||
return None
|
return None, error
|
||||||
|
|||||||
+180
-89
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
@@ -22,52 +21,65 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import Blueprint, request, flash, redirect, url_for
|
from flask import Blueprint, request, flash, redirect, 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 login_required, current_user
|
||||||
from sqlalchemy.sql.expression import func, or_, and_
|
from sqlalchemy.sql.expression import func
|
||||||
|
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||||
|
|
||||||
from . import logger, ub, searched_ids, db
|
from . import logger, ub, searched_ids, calibre_db
|
||||||
from .web import render_title_template
|
from .web import login_required_if_no_ano, render_title_template
|
||||||
from .helper import common_filters
|
|
||||||
|
|
||||||
|
|
||||||
shelf = Blueprint('shelf', __name__)
|
shelf = Blueprint('shelf', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def check_shelf_edit_permissions(cur_shelf):
|
||||||
|
if not cur_shelf.is_public and not cur_shelf.user_id == int(current_user.id):
|
||||||
|
log.error("User %s not allowed to edit shelf %s", current_user, cur_shelf)
|
||||||
|
return False
|
||||||
|
if cur_shelf.is_public and not current_user.role_edit_shelfs():
|
||||||
|
log.info("User %s not allowed to edit public shelves", current_user)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_shelf_view_permissions(cur_shelf):
|
||||||
|
if cur_shelf.is_public:
|
||||||
|
return True
|
||||||
|
if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
|
||||||
|
log.error("User is unauthorized to view non-public shelf: %s", cur_shelf)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
|
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def add_to_shelf(shelf_id, book_id):
|
def add_to_shelf(shelf_id, book_id):
|
||||||
|
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: %s", shelf_id)
|
||||||
if not request.is_xhr:
|
if not xhr:
|
||||||
flash(_(u"Invalid shelf specified"), category="error")
|
flash(_(u"Invalid shelf specified"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Invalid shelf specified", 400
|
return "Invalid shelf specified", 400
|
||||||
|
|
||||||
if not shelf.is_public and not shelf.user_id == int(current_user.id):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
log.error("User %s not allowed to add a book to %s", current_user, shelf)
|
if not xhr:
|
||||||
if not request.is_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 the the shelf: %(shelfname)s", shelfname=shelf.name),
|
||||||
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 the shelf: %s" % shelf.name, 403
|
||||||
|
|
||||||
if shelf.is_public and not current_user.role_edit_shelfs():
|
|
||||||
log.info("User %s not allowed to edit public shelves", current_user)
|
|
||||||
if not request.is_xhr:
|
|
||||||
flash(_(u"You are not allowed to edit public shelves"), category="error")
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
return "User is not allowed to edit public shelves", 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()
|
||||||
if book_in_shelf:
|
if book_in_shelf:
|
||||||
log.error("Book %s is already part of %s", book_id, shelf)
|
log.error("Book %s is already part of %s", book_id, shelf)
|
||||||
if not request.is_xhr:
|
if not xhr:
|
||||||
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
|
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Book is already part of the shelf: %s" % shelf.name, 400
|
return "Book is already part of the shelf: %s" % shelf.name, 400
|
||||||
@@ -78,10 +90,19 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
else:
|
else:
|
||||||
maxOrder = maxOrder[0]
|
maxOrder = maxOrder[0]
|
||||||
|
|
||||||
ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1)
|
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1))
|
||||||
ub.session.add(ins)
|
shelf.last_modified = datetime.utcnow()
|
||||||
|
try:
|
||||||
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
if not request.is_xhr:
|
except (OperationalError, InvalidRequestError):
|
||||||
|
ub.session.rollback()
|
||||||
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
|
if "HTTP_REFERER" in request.environ:
|
||||||
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
|
else:
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
if not xhr:
|
||||||
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"])
|
||||||
@@ -99,16 +120,10 @@ def search_to_shelf(shelf_id):
|
|||||||
flash(_(u"Invalid shelf specified"), category="error")
|
flash(_(u"Invalid shelf specified"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
if not shelf.is_public and not shelf.user_id == int(current_user.id):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
log.error("User %s not allowed to add a book to %s", current_user, shelf)
|
|
||||||
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'))
|
||||||
|
|
||||||
if shelf.is_public and not current_user.role_edit_shelfs():
|
|
||||||
log.error("User %s not allowed to edit public shelves", current_user)
|
|
||||||
flash(_(u"User is not allowed to edit public shelves"), category="error")
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
|
|
||||||
if current_user.id in searched_ids and searched_ids[current_user.id]:
|
if current_user.id in searched_ids and searched_ids[current_user.id]:
|
||||||
books_for_shelf = list()
|
books_for_shelf = list()
|
||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
|
||||||
@@ -135,10 +150,15 @@ def search_to_shelf(shelf_id):
|
|||||||
|
|
||||||
for book in books_for_shelf:
|
for book in books_for_shelf:
|
||||||
maxOrder = maxOrder + 1
|
maxOrder = maxOrder + 1
|
||||||
ins = ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder)
|
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
|
||||||
ub.session.add(ins)
|
shelf.last_modified = datetime.utcnow()
|
||||||
|
try:
|
||||||
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
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):
|
||||||
|
ub.session.rollback()
|
||||||
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
else:
|
else:
|
||||||
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'))
|
||||||
@@ -147,10 +167,11 @@ def search_to_shelf(shelf_id):
|
|||||||
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>")
|
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def remove_from_shelf(shelf_id, book_id):
|
def remove_from_shelf(shelf_id, book_id):
|
||||||
|
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: %s", shelf_id)
|
||||||
if not request.is_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
|
||||||
|
|
||||||
@@ -162,34 +183,42 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
# true 0 x 1
|
# true 0 x 1
|
||||||
# false 0 x 0
|
# false 0 x 0
|
||||||
|
|
||||||
if (not shelf.is_public and shelf.user_id == int(current_user.id)) \
|
if check_shelf_edit_permissions(shelf):
|
||||||
or (shelf.is_public and current_user.role_edit_shelfs()):
|
|
||||||
book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
book_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()
|
||||||
|
|
||||||
if book_shelf is None:
|
if book_shelf is None:
|
||||||
log.error("Book %s already removed from %s", book_id, shelf)
|
log.error("Book %s already removed from %s", book_id, shelf)
|
||||||
if not request.is_xhr:
|
if not xhr:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Book already removed from shelf", 410
|
return "Book already removed from shelf", 410
|
||||||
|
|
||||||
|
try:
|
||||||
ub.session.delete(book_shelf)
|
ub.session.delete(book_shelf)
|
||||||
|
shelf.last_modified = datetime.utcnow()
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
|
except (OperationalError, InvalidRequestError):
|
||||||
if not request.is_xhr:
|
ub.session.rollback()
|
||||||
flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
|
else:
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
if not xhr:
|
||||||
|
flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
|
if "HTTP_REFERER" in request.environ:
|
||||||
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
|
else:
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
return "", 204
|
return "", 204
|
||||||
else:
|
else:
|
||||||
log.error("User %s not allowed to remove a book from %s", current_user, shelf)
|
if not xhr:
|
||||||
if not request.is_xhr:
|
|
||||||
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'))
|
||||||
return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403
|
return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/create", methods=["GET", "POST"])
|
@shelf.route("/shelf/create", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def create_shelf():
|
def create_shelf():
|
||||||
@@ -200,21 +229,41 @@ def create_shelf():
|
|||||||
shelf.is_public = 1
|
shelf.is_public = 1
|
||||||
shelf.name = to_save["title"]
|
shelf.name = to_save["title"]
|
||||||
shelf.user_id = int(current_user.id)
|
shelf.user_id = int(current_user.id)
|
||||||
existing_shelf = ub.session.query(ub.Shelf).filter(
|
|
||||||
or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1),
|
is_shelf_name_unique = False
|
||||||
(ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).first()
|
if shelf.is_public == 1:
|
||||||
if existing_shelf:
|
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||||
flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error")
|
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1)) \
|
||||||
|
.first() is None
|
||||||
|
|
||||||
|
if not is_shelf_name_unique:
|
||||||
|
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
||||||
|
category="error")
|
||||||
else:
|
else:
|
||||||
|
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||||
|
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) &
|
||||||
|
(ub.Shelf.user_id == int(current_user.id)))\
|
||||||
|
.first() is None
|
||||||
|
|
||||||
|
if not is_shelf_name_unique:
|
||||||
|
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
||||||
|
category="error")
|
||||||
|
|
||||||
|
if is_shelf_name_unique:
|
||||||
try:
|
try:
|
||||||
ub.session.add(shelf)
|
ub.session.add(shelf)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success")
|
flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success")
|
||||||
|
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
||||||
|
except (OperationalError, InvalidRequestError):
|
||||||
|
ub.session.rollback()
|
||||||
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
ub.session.rollback()
|
||||||
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=_(u"create a shelf"), page="shelfcreate")
|
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Create a Shelf"), page="shelfcreate")
|
||||||
else:
|
else:
|
||||||
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate")
|
return render_title_template('shelf_edit.html', shelf=shelf, 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"])
|
||||||
@@ -223,14 +272,31 @@ 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()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
existing_shelf = ub.session.query(ub.Shelf).filter(
|
|
||||||
or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1),
|
is_shelf_name_unique = False
|
||||||
(ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).filter(
|
if shelf.is_public == 1:
|
||||||
ub.Shelf.id != shelf_id).first()
|
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||||
if existing_shelf:
|
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1)) \
|
||||||
flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error")
|
.filter(ub.Shelf.id != shelf_id) \
|
||||||
|
.first() is None
|
||||||
|
|
||||||
|
if not is_shelf_name_unique:
|
||||||
|
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
||||||
|
category="error")
|
||||||
else:
|
else:
|
||||||
|
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||||
|
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) &
|
||||||
|
(ub.Shelf.user_id == int(current_user.id)))\
|
||||||
|
.filter(ub.Shelf.id != shelf_id)\
|
||||||
|
.first() is None
|
||||||
|
|
||||||
|
if not is_shelf_name_unique:
|
||||||
|
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
||||||
|
category="error")
|
||||||
|
|
||||||
|
if is_shelf_name_unique:
|
||||||
shelf.name = to_save["title"]
|
shelf.name = to_save["title"]
|
||||||
|
shelf.last_modified = datetime.utcnow()
|
||||||
if "is_public" in to_save:
|
if "is_public" in to_save:
|
||||||
shelf.is_public = 1
|
shelf.is_public = 1
|
||||||
else:
|
else:
|
||||||
@@ -238,54 +304,68 @@ def edit_shelf(shelf_id):
|
|||||||
try:
|
try:
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success")
|
flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success")
|
||||||
|
except (OperationalError, InvalidRequestError):
|
||||||
|
ub.session.rollback()
|
||||||
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
ub.session.rollback()
|
||||||
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=_(u"Edit a shelf"), page="shelfedit")
|
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
|
||||||
else:
|
else:
|
||||||
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
|
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_shelf_helper(cur_shelf):
|
||||||
|
if not cur_shelf or not check_shelf_edit_permissions(cur_shelf):
|
||||||
|
return
|
||||||
|
shelf_id = cur_shelf.id
|
||||||
|
ub.session.delete(cur_shelf)
|
||||||
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
||||||
|
ub.session.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id))
|
||||||
|
ub.session.commit()
|
||||||
|
log.info("successfully deleted %s", cur_shelf)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/delete/<int:shelf_id>")
|
@shelf.route("/shelf/delete/<int:shelf_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def delete_shelf(shelf_id):
|
def delete_shelf(shelf_id):
|
||||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
deleted = None
|
try:
|
||||||
if current_user.role_admin():
|
delete_shelf_helper(cur_shelf)
|
||||||
deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete()
|
except (OperationalError, InvalidRequestError):
|
||||||
else:
|
ub.session.rollback()
|
||||||
if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
or (cur_shelf.is_public and current_user.role_edit_shelfs()):
|
|
||||||
deleted = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
|
||||||
ub.Shelf.id == shelf_id),
|
|
||||||
and_(ub.Shelf.is_public == 1,
|
|
||||||
ub.Shelf.id == shelf_id))).delete()
|
|
||||||
|
|
||||||
if deleted:
|
|
||||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
|
||||||
ub.session.commit()
|
|
||||||
log.info("successfully deleted %s", cur_shelf)
|
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
# @shelf.route("/shelfdown/<int:shelf_id>")
|
|
||||||
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
|
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
|
||||||
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
|
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
|
||||||
|
@login_required_if_no_ano
|
||||||
def show_shelf(shelf_type, shelf_id):
|
def show_shelf(shelf_type, shelf_id):
|
||||||
if current_user.is_anonymous:
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
|
|
||||||
else:
|
|
||||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
|
||||||
ub.Shelf.id == shelf_id),
|
|
||||||
and_(ub.Shelf.is_public == 1,
|
|
||||||
ub.Shelf.id == shelf_id))).first()
|
|
||||||
result = list()
|
result = list()
|
||||||
# user is allowed to access shelf
|
# user is allowed to access shelf
|
||||||
if shelf:
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
|
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
|
||||||
|
|
||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
|
||||||
.order_by(ub.BookShelf.order.asc()).all()
|
.order_by(ub.BookShelf.order.asc()).all()
|
||||||
books_list = [ b.book_id for b in books_in_shelf]
|
for book in books_in_shelf:
|
||||||
result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all()
|
cur_book = calibre_db.get_filtered_book(book.book_id)
|
||||||
|
if cur_book:
|
||||||
|
result.append(cur_book)
|
||||||
|
else:
|
||||||
|
cur_book = calibre_db.get_book(book.book_id)
|
||||||
|
if not cur_book:
|
||||||
|
log.info('Not existing book %s in %s deleted', book.book_id, shelf)
|
||||||
|
try:
|
||||||
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete()
|
||||||
|
ub.session.commit()
|
||||||
|
except (OperationalError, InvalidRequestError):
|
||||||
|
ub.session.rollback()
|
||||||
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name),
|
return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name),
|
||||||
shelf=shelf, page="shelf")
|
shelf=shelf, page="shelf")
|
||||||
else:
|
else:
|
||||||
@@ -293,7 +373,6 @@ def show_shelf(shelf_type, shelf_id):
|
|||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def order_shelf(shelf_id):
|
def order_shelf(shelf_id):
|
||||||
@@ -305,20 +384,32 @@ def order_shelf(shelf_id):
|
|||||||
for book in books_in_shelf:
|
for book in books_in_shelf:
|
||||||
setattr(book, 'order', to_save[str(book.book_id)])
|
setattr(book, 'order', to_save[str(book.book_id)])
|
||||||
counter += 1
|
counter += 1
|
||||||
|
# if order diffrent from before -> shelf.last_modified = datetime.utcnow()
|
||||||
|
try:
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
if current_user.is_anonymous:
|
except (OperationalError, InvalidRequestError):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
|
ub.session.rollback()
|
||||||
else:
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
|
||||||
ub.Shelf.id == shelf_id),
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
and_(ub.Shelf.is_public == 1,
|
|
||||||
ub.Shelf.id == shelf_id))).first()
|
|
||||||
result = list()
|
result = list()
|
||||||
if shelf:
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||||
.order_by(ub.BookShelf.order.asc()).all()
|
.order_by(ub.BookShelf.order.asc()).all()
|
||||||
books_list = [ b.book_id for b in books_in_shelf2]
|
for book in books_in_shelf2:
|
||||||
result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all()
|
cur_book = calibre_db.get_filtered_book(book.book_id)
|
||||||
|
if cur_book:
|
||||||
|
result.append({'title': cur_book.title,
|
||||||
|
'id': cur_book.id,
|
||||||
|
'author': cur_book.authors,
|
||||||
|
'series': cur_book.series,
|
||||||
|
'series_index': cur_book.series_index})
|
||||||
|
else:
|
||||||
|
cur_book = calibre_db.get_book(book.book_id)
|
||||||
|
result.append({'title': _('Hidden Book'),
|
||||||
|
'id': cur_book.id,
|
||||||
|
'author': [],
|
||||||
|
'series': []})
|
||||||
return render_title_template('shelf_order.html', entries=result,
|
return render_title_template('shelf_order.html', entries=result,
|
||||||
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
||||||
shelf=shelf, page="shelforder")
|
shelf=shelf, page="shelforder")
|
||||||
|
|||||||
Vendored
+7976
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
|||||||
|
body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover .badge{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #cc7b19;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
box-shadow: 0 0 4px rgba(0,0,0,.6);
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.cover{
|
||||||
|
box-shadow: 0 0 4px rgba(0,0,0,.6);
|
||||||
|
}
|
||||||
+12
-14
@@ -4,7 +4,7 @@ body {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
color: white;
|
color: white;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
margin: 0px;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
@@ -13,7 +13,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.view {
|
.view {
|
||||||
padding-top:0px;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a,
|
#sidebar a,
|
||||||
@@ -34,13 +34,13 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
transition: all .2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a:hover,
|
#sidebar a:hover,
|
||||||
#sidebar a:focus {
|
#sidebar a:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0px 2px 8px 1px black;
|
box-shadow: 0 2px 8px 1px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.active,
|
#sidebar a.active,
|
||||||
@@ -79,7 +79,6 @@ body {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
transition: min-height 150ms ease-in-out;
|
transition: min-height 150ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,18 +91,17 @@ body {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
||||||
transition: width 150ms ease-in-out;
|
transition: width 150ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#progress .bar-load {
|
#progress .bar-load {
|
||||||
color: #000;
|
color: #000;
|
||||||
background-color: #CCC;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
#progress .bar-read {
|
#progress .bar-read {
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
background-color: #45B29D;
|
background-color: #45b29d;
|
||||||
}
|
}
|
||||||
|
|
||||||
#progress .text {
|
#progress .text {
|
||||||
@@ -152,7 +150,8 @@ body {
|
|||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th,
|
||||||
|
td {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,18 +205,17 @@ th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark-theme #titlebar {
|
.dark-theme #titlebar {
|
||||||
color: #DDD;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-theme #titlebar a:active {
|
.dark-theme #titlebar a:active {
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-theme #progress .bar-read {
|
.dark-theme #progress .bar-read {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dark-theme .overlay {
|
.dark-theme .overlay {
|
||||||
background-color: rgba(0,0,0,0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
File diff suppressed because one or more lines are too long
Vendored
+68
-8
@@ -230,36 +230,46 @@
|
|||||||
z-index: 200;
|
z-index: 200;
|
||||||
max-width: 20em;
|
max-width: 20em;
|
||||||
background-color: #FFFF99;
|
background-color: #FFFF99;
|
||||||
box-shadow: 0px 2px 5px #333;
|
box-shadow: 0px 2px 5px #888;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0.6em;
|
padding: 6px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: message-box;
|
font: message-box;
|
||||||
|
font-size: 9px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.annotationLayer .popup > * {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
.annotationLayer .popup h1 {
|
.annotationLayer .popup h1 {
|
||||||
font-size: 1em;
|
display: inline-block;
|
||||||
border-bottom: 1px solid #000000;
|
}
|
||||||
margin: 0;
|
|
||||||
padding-bottom: 0.2em;
|
.annotationLayer .popup span {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationLayer .popup p {
|
.annotationLayer .popup p {
|
||||||
margin: 0;
|
border-top: 1px solid #333;
|
||||||
padding-top: 0.2em;
|
margin-top: 2px;
|
||||||
|
padding-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationLayer .highlightAnnotation,
|
.annotationLayer .highlightAnnotation,
|
||||||
.annotationLayer .underlineAnnotation,
|
.annotationLayer .underlineAnnotation,
|
||||||
.annotationLayer .squigglyAnnotation,
|
.annotationLayer .squigglyAnnotation,
|
||||||
.annotationLayer .strikeoutAnnotation,
|
.annotationLayer .strikeoutAnnotation,
|
||||||
|
.annotationLayer .freeTextAnnotation,
|
||||||
.annotationLayer .lineAnnotation svg line,
|
.annotationLayer .lineAnnotation svg line,
|
||||||
.annotationLayer .squareAnnotation svg rect,
|
.annotationLayer .squareAnnotation svg rect,
|
||||||
.annotationLayer .circleAnnotation svg ellipse,
|
.annotationLayer .circleAnnotation svg ellipse,
|
||||||
.annotationLayer .polylineAnnotation svg polyline,
|
.annotationLayer .polylineAnnotation svg polyline,
|
||||||
.annotationLayer .polygonAnnotation svg polygon,
|
.annotationLayer .polygonAnnotation svg polygon,
|
||||||
|
.annotationLayer .caretAnnotation,
|
||||||
.annotationLayer .inkAnnotation svg polyline,
|
.annotationLayer .inkAnnotation svg polyline,
|
||||||
.annotationLayer .stampAnnotation,
|
.annotationLayer .stampAnnotation,
|
||||||
.annotationLayer .fileAttachmentAnnotation {
|
.annotationLayer .fileAttachmentAnnotation {
|
||||||
@@ -279,6 +289,7 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
border: 9px solid transparent;
|
border: 9px solid transparent;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
|
-webkit-border-image: url(images/shadow.png) 9 9 repeat;
|
||||||
-o-border-image: url(images/shadow.png) 9 9 repeat;
|
-o-border-image: url(images/shadow.png) 9 9 repeat;
|
||||||
border-image: url(images/shadow.png) 9 9 repeat;
|
border-image: url(images/shadow.png) 9 9 repeat;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
@@ -543,15 +554,20 @@ select {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
border-top: 1px solid #333;
|
border-top: 1px solid #333;
|
||||||
|
|
||||||
|
-webkit-transition-duration: 200ms;
|
||||||
|
|
||||||
transition-duration: 200ms;
|
transition-duration: 200ms;
|
||||||
|
-webkit-transition-timing-function: ease;
|
||||||
transition-timing-function: ease;
|
transition-timing-function: ease;
|
||||||
}
|
}
|
||||||
html[dir='ltr'] #sidebarContainer {
|
html[dir='ltr'] #sidebarContainer {
|
||||||
|
-webkit-transition-property: left;
|
||||||
transition-property: left;
|
transition-property: left;
|
||||||
left: -200px;
|
left: -200px;
|
||||||
left: calc(-1 * var(--sidebar-width));
|
left: calc(-1 * var(--sidebar-width));
|
||||||
}
|
}
|
||||||
html[dir='rtl'] #sidebarContainer {
|
html[dir='rtl'] #sidebarContainer {
|
||||||
|
-webkit-transition-property: right;
|
||||||
transition-property: right;
|
transition-property: right;
|
||||||
right: -200px;
|
right: -200px;
|
||||||
right: calc(-1 * var(--sidebar-width));
|
right: calc(-1 * var(--sidebar-width));
|
||||||
@@ -563,6 +579,7 @@ html[dir='rtl'] #sidebarContainer {
|
|||||||
|
|
||||||
#outerContainer.sidebarResizing #sidebarContainer {
|
#outerContainer.sidebarResizing #sidebarContainer {
|
||||||
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
|
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
|
||||||
|
-webkit-transition-duration: 0s;
|
||||||
transition-duration: 0s;
|
transition-duration: 0s;
|
||||||
/* Prevent e.g. the thumbnails being selected when the sidebar is resized. */
|
/* Prevent e.g. the thumbnails being selected when the sidebar is resized. */
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
@@ -620,7 +637,9 @@ html[dir='rtl'] #sidebarContent {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
#viewerContainer:not(.pdfPresentationMode) {
|
#viewerContainer:not(.pdfPresentationMode) {
|
||||||
|
-webkit-transition-duration: 200ms;
|
||||||
transition-duration: 200ms;
|
transition-duration: 200ms;
|
||||||
|
-webkit-transition-timing-function: ease;
|
||||||
transition-timing-function: ease;
|
transition-timing-function: ease;
|
||||||
}
|
}
|
||||||
html[dir='ltr'] #viewerContainer {
|
html[dir='ltr'] #viewerContainer {
|
||||||
@@ -632,15 +651,18 @@ html[dir='rtl'] #viewerContainer {
|
|||||||
|
|
||||||
#outerContainer.sidebarResizing #viewerContainer {
|
#outerContainer.sidebarResizing #viewerContainer {
|
||||||
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
|
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
|
||||||
|
-webkit-transition-duration: 0s;
|
||||||
transition-duration: 0s;
|
transition-duration: 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
|
html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
|
||||||
|
-webkit-transition-property: left;
|
||||||
transition-property: left;
|
transition-property: left;
|
||||||
left: 200px;
|
left: 200px;
|
||||||
left: var(--sidebar-width);
|
left: var(--sidebar-width);
|
||||||
}
|
}
|
||||||
html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
|
html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
|
||||||
|
-webkit-transition-property: right;
|
||||||
transition-property: right;
|
transition-property: right;
|
||||||
right: 200px;
|
right: 200px;
|
||||||
right: var(--sidebar-width);
|
right: var(--sidebar-width);
|
||||||
@@ -662,6 +684,8 @@ html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentatio
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
background-color: #424242; /* fallback */
|
background-color: #424242; /* fallback */
|
||||||
|
background-image: url(images/texture.png),
|
||||||
|
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,30%,.99)), to(hsla(0,0%,25%,.95)));
|
||||||
background-image: url(images/texture.png),
|
background-image: url(images/texture.png),
|
||||||
linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
|
linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
|
||||||
}
|
}
|
||||||
@@ -697,6 +721,8 @@ html[dir='rtl'] #sidebarResizer {
|
|||||||
position: relative;
|
position: relative;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
background-color: #474747; /* fallback */
|
background-color: #474747; /* fallback */
|
||||||
|
background-image: url(images/texture.png),
|
||||||
|
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,32%,.99)), to(hsla(0,0%,27%,.95)));
|
||||||
background-image: url(images/texture.png),
|
background-image: url(images/texture.png),
|
||||||
linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
|
linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
|
||||||
}
|
}
|
||||||
@@ -733,6 +759,7 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
-webkit-transition: width 200ms;
|
||||||
transition: width 200ms;
|
transition: width 200ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,6 +775,7 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar {
|
|||||||
|
|
||||||
#loadingBar .progress.indeterminate {
|
#loadingBar .progress.indeterminate {
|
||||||
background-color: #999;
|
background-color: #999;
|
||||||
|
-webkit-transition: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,6 +843,9 @@ html[dir='rtl'] .findbar {
|
|||||||
#findInput::-webkit-input-placeholder {
|
#findInput::-webkit-input-placeholder {
|
||||||
color: hsl(0, 0%, 75%);
|
color: hsl(0, 0%, 75%);
|
||||||
}
|
}
|
||||||
|
#findInput::-moz-placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
#findInput:-ms-input-placeholder {
|
#findInput:-ms-input-placeholder {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
@@ -1006,6 +1037,7 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton {
|
|||||||
.splitToolbarButton.toggled > .toolbarButton,
|
.splitToolbarButton.toggled > .toolbarButton,
|
||||||
.toolbarButton.textButton {
|
.toolbarButton.textButton {
|
||||||
background-color: hsla(0,0%,0%,.12);
|
background-color: hsla(0,0%,0%,.12);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border: 1px solid hsla(0,0%,0%,.35);
|
border: 1px solid hsla(0,0%,0%,.35);
|
||||||
@@ -1013,8 +1045,11 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton {
|
|||||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||||
0 0 1px hsla(0,0%,100%,.15) inset,
|
0 0 1px hsla(0,0%,100%,.15) inset,
|
||||||
0 1px 0 hsla(0,0%,100%,.05);
|
0 1px 0 hsla(0,0%,100%,.05);
|
||||||
|
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||||
transition-property: background-color, border-color, box-shadow;
|
transition-property: background-color, border-color, box-shadow;
|
||||||
|
-webkit-transition-duration: 150ms;
|
||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
|
-webkit-transition-timing-function: ease;
|
||||||
transition-timing-function: ease;
|
transition-timing-function: ease;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1072,8 +1107,11 @@ html[dir='rtl'] .splitToolbarButtonSeparator {
|
|||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
margin: 1px 0;
|
margin: 1px 0;
|
||||||
box-shadow: 0 0 0 1px hsla(0,0%,100%,.03);
|
box-shadow: 0 0 0 1px hsla(0,0%,100%,.03);
|
||||||
|
-webkit-transition-property: padding;
|
||||||
transition-property: padding;
|
transition-property: padding;
|
||||||
|
-webkit-transition-duration: 10ms;
|
||||||
transition-duration: 10ms;
|
transition-duration: 10ms;
|
||||||
|
-webkit-transition-timing-function: ease;
|
||||||
transition-timing-function: ease;
|
transition-timing-function: ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,8 +1132,11 @@ html[dir='rtl'] .splitToolbarButtonSeparator {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
/* Opera does not support user-select, use <... unselectable="on"> instead */
|
/* Opera does not support user-select, use <... unselectable="on"> instead */
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||||
transition-property: background-color, border-color, box-shadow;
|
transition-property: background-color, border-color, box-shadow;
|
||||||
|
-webkit-transition-duration: 150ms;
|
||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
|
-webkit-transition-timing-function: ease;
|
||||||
transition-timing-function: ease;
|
transition-timing-function: ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,6 +1158,7 @@ html[dir='rtl'] .dropdownToolbarButton {
|
|||||||
.secondaryToolbarButton:hover,
|
.secondaryToolbarButton:hover,
|
||||||
.secondaryToolbarButton:focus {
|
.secondaryToolbarButton:focus {
|
||||||
background-color: hsla(0,0%,0%,.12);
|
background-color: hsla(0,0%,0%,.12);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border: 1px solid hsla(0,0%,0%,.35);
|
border: 1px solid hsla(0,0%,0%,.35);
|
||||||
@@ -1131,13 +1173,17 @@ html[dir='rtl'] .dropdownToolbarButton {
|
|||||||
.dropdownToolbarButton:hover:active,
|
.dropdownToolbarButton:hover:active,
|
||||||
.secondaryToolbarButton:hover:active {
|
.secondaryToolbarButton:hover:active {
|
||||||
background-color: hsla(0,0%,0%,.2);
|
background-color: hsla(0,0%,0%,.2);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45);
|
border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45);
|
||||||
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
|
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
|
||||||
0 0 1px hsla(0,0%,0%,.2) inset,
|
0 0 1px hsla(0,0%,0%,.2) inset,
|
||||||
0 1px 0 hsla(0,0%,100%,.05);
|
0 1px 0 hsla(0,0%,100%,.05);
|
||||||
|
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||||
transition-property: background-color, border-color, box-shadow;
|
transition-property: background-color, border-color, box-shadow;
|
||||||
|
-webkit-transition-duration: 10ms;
|
||||||
transition-duration: 10ms;
|
transition-duration: 10ms;
|
||||||
|
-webkit-transition-timing-function: linear;
|
||||||
transition-timing-function: linear;
|
transition-timing-function: linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1145,13 +1191,17 @@ html[dir='rtl'] .dropdownToolbarButton {
|
|||||||
.splitToolbarButton.toggled > .toolbarButton.toggled,
|
.splitToolbarButton.toggled > .toolbarButton.toggled,
|
||||||
.secondaryToolbarButton.toggled {
|
.secondaryToolbarButton.toggled {
|
||||||
background-color: hsla(0,0%,0%,.3);
|
background-color: hsla(0,0%,0%,.3);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5);
|
border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5);
|
||||||
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
|
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
|
||||||
0 0 1px hsla(0,0%,0%,.2) inset,
|
0 0 1px hsla(0,0%,0%,.2) inset,
|
||||||
0 1px 0 hsla(0,0%,100%,.05);
|
0 1px 0 hsla(0,0%,100%,.05);
|
||||||
|
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||||
transition-property: background-color, border-color, box-shadow;
|
transition-property: background-color, border-color, box-shadow;
|
||||||
|
-webkit-transition-duration: 10ms;
|
||||||
transition-duration: 10ms;
|
transition-duration: 10ms;
|
||||||
|
-webkit-transition-timing-function: linear;
|
||||||
transition-timing-function: linear;
|
transition-timing-function: linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1493,6 +1543,7 @@ html[dir='rtl'] .verticalToolbarSeparator {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: hsla(0,0%,100%,.09);
|
background-color: hsla(0,0%,100%,.09);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border: 1px solid hsla(0,0%,0%,.35);
|
border: 1px solid hsla(0,0%,0%,.35);
|
||||||
@@ -1503,8 +1554,11 @@ html[dir='rtl'] .verticalToolbarSeparator {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
outline-style: none;
|
outline-style: none;
|
||||||
|
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||||
transition-property: background-color, border-color, box-shadow;
|
transition-property: background-color, border-color, box-shadow;
|
||||||
|
-webkit-transition-duration: 150ms;
|
||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
|
-webkit-transition-timing-function: ease;
|
||||||
transition-timing-function: ease;
|
transition-timing-function: ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1619,6 +1673,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing > .thumbnailImage,
|
|||||||
a:focus > .thumbnail > .thumbnailSelectionRing,
|
a:focus > .thumbnail > .thumbnailSelectionRing,
|
||||||
.thumbnail:hover > .thumbnailSelectionRing {
|
.thumbnail:hover > .thumbnailSelectionRing {
|
||||||
background-color: hsla(0,0%,100%,.15);
|
background-color: hsla(0,0%,100%,.15);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||||
@@ -1634,6 +1689,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing,
|
|||||||
|
|
||||||
.thumbnail.selected > .thumbnailSelectionRing {
|
.thumbnail.selected > .thumbnailSelectionRing {
|
||||||
background-color: hsla(0,0%,100%,.3);
|
background-color: hsla(0,0%,100%,.3);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||||
@@ -1755,6 +1811,7 @@ html[dir='rtl'] .outlineItemToggler::before {
|
|||||||
.outlineItem > a:hover,
|
.outlineItem > a:hover,
|
||||||
.attachmentsItem > button:hover {
|
.attachmentsItem > button:hover {
|
||||||
background-color: hsla(0,0%,100%,.02);
|
background-color: hsla(0,0%,100%,.02);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||||
@@ -1766,6 +1823,7 @@ html[dir='rtl'] .outlineItemToggler::before {
|
|||||||
|
|
||||||
.outlineItem.selected {
|
.outlineItem.selected {
|
||||||
background-color: hsla(0,0%,100%,.08);
|
background-color: hsla(0,0%,100%,.08);
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||||
@@ -1850,6 +1908,8 @@ html[dir='rtl'] .outlineItemToggler::before {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
background-color: #474747; /* fallback */
|
background-color: #474747; /* fallback */
|
||||||
|
background-image: url(images/texture.png),
|
||||||
|
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,32%,.99)), to(hsla(0,0%,27%,.95)));
|
||||||
background-image: url(images/texture.png),
|
background-image: url(images/texture.png),
|
||||||
linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
|
linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
|
||||||
box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08),
|
box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08),
|
||||||
|
|||||||
+38
-41
@@ -1,84 +1,81 @@
|
|||||||
.sm2-bar-ui {
|
.sm2-bar-ui {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm2-bar-ui.compact {
|
.sm2-bar-ui.compact {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm2-progress .sm2-progress-ball {
|
.sm2-progress .sm2-progress-ball {
|
||||||
width: .5333em;
|
width: 0.5333em;
|
||||||
height: 1.9333em;
|
height: 1.9333em;
|
||||||
border-radius: 0em;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm2-progress .sm2-progress-track {
|
.sm2-progress .sm2-progress-track {
|
||||||
height: 0.15em;
|
height: 0.15em;
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm2-bar-ui .sm2-main-controls,
|
.sm2-bar-ui .sm2-main-controls,
|
||||||
.sm2-bar-ui .sm2-playlist-drawer {
|
.sm2-bar-ui .sm2-playlist-drawer {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm2-bar-ui .sm2-inline-texture {
|
.sm2-bar-ui .sm2-inline-texture {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating .glyphicon-star {
|
.rating .glyphicon-star {
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating .glyphicon-star.good {
|
.rating .glyphicon-star.good {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #272B30;
|
background: #272b30;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#area {
|
#area {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
margin: 5% auto;
|
margin: 5% auto;
|
||||||
max-width: 1250px;
|
max-width: 1250px;
|
||||||
}
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#area iframe {
|
#area iframe {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#prev {
|
#prev {
|
||||||
left: 40px;
|
left: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#next {
|
#next {
|
||||||
right: 40px;
|
right: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
xmp,
|
xmp,
|
||||||
pre,
|
pre,
|
||||||
plaintext {
|
plaintext {
|
||||||
display: block;
|
display: block;
|
||||||
font-family: -moz-fixed;
|
font-family: -moz-fixed;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#area {
|
pre {
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
font-family: -moz-fixed;
|
font-family: -moz-fixed;
|
||||||
@@ -89,4 +86,4 @@
|
|||||||
-moz-column-gap: 20px;
|
-moz-column-gap: 20px;
|
||||||
-webkit-column-gap: 20px;
|
-webkit-column-gap: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-116
@@ -1,7 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'fontello';
|
font-family: 'fontello';
|
||||||
src: url('fonts/fontello.eot?60518104');
|
src: url('fonts/fontello.eot?60518104');
|
||||||
src: url('fonts/fontello.eot?60518104#iefix') format('embedded-opentype'),
|
src:
|
||||||
|
url('fonts/fontello.eot?60518104#iefix') format('embedded-opentype'),
|
||||||
url('fonts/fontello.woff?60518104') format('woff'),
|
url('fonts/fontello.woff?60518104') format('woff'),
|
||||||
url('fonts/fontello.ttf?60518104') format('truetype'),
|
url('fonts/fontello.ttf?60518104') format('truetype'),
|
||||||
url('fonts/fontello.svg?60518104#fontello') format('svg');
|
url('fonts/fontello.svg?60518104#fontello') format('svg');
|
||||||
@@ -22,17 +23,15 @@ body {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-transition: -webkit-transform .4s, width .2s;
|
-webkit-transition: -webkit-transform 0.4s, width 0.2s;
|
||||||
-moz-transition: -webkit-transform .4s, width .2s;
|
-moz-transition: -webkit-transform 0.4s, width 0.2s;
|
||||||
-ms-transition: -webkit-transform .4s, width .2s;
|
-ms-transition: -webkit-transform 0.4s, width 0.2s;
|
||||||
|
-moz-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||||
-moz-box-shadow: inset 0 0 50px rgba(0,0,0,.1);
|
-webkit-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||||
-webkit-box-shadow: inset 0 0 50px rgba(0,0,0,.1);
|
-ms-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||||
-ms-box-shadow: inset 0 0 50px rgba(0,0,0,.1);
|
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||||
box-shadow: inset 0 0 50px rgba(0,0,0,.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#titlebar {
|
#titlebar {
|
||||||
height: 8%;
|
height: 8%;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
@@ -42,11 +41,11 @@ body {
|
|||||||
color: #4f4f4f;
|
color: #4f4f4f;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
font-family: Georgia, "Times New Roman", Times, serif;
|
font-family: Georgia, "Times New Roman", Times, serif;
|
||||||
opacity: .5;
|
opacity: 0.5;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
-webkit-transition: opacity .5s;
|
-webkit-transition: opacity 0.5s;
|
||||||
-moz-transition: opacity .5s;
|
-moz-transition: opacity 0.5s;
|
||||||
-ms-transition: opacity .5s;
|
-ms-transition: opacity 0.5s;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ body {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
opacity: .5;
|
opacity: 0.5;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -70,35 +69,27 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#titlebar a:hover {
|
#titlebar a:hover {
|
||||||
opacity: .8;
|
opacity: 0.8;
|
||||||
border: 1px rgba(0,0,0,.2) solid;
|
border: 1px rgba(0, 0, 0, 0.2) solid;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#titlebar a:active {
|
#titlebar a:active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: rgba(0,0,0,.6);
|
color: rgba(0, 0, 0, 0.6);
|
||||||
/* margin: 1px -1px -1px 1px; */
|
-moz-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
-moz-box-shadow: inset 0 0 6px rgba(155,155,155,.8);
|
-webkit-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(155,155,155,.8);
|
-ms-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
-ms-box-shadow: inset 0 0 6px rgba(155,155,155,.8);
|
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
box-shadow: inset 0 0 6px rgba(155,155,155,.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#book-title {
|
#book-title { font-weight: 600; }
|
||||||
font-weight: 600;
|
#title-seperator { display: none; }
|
||||||
}
|
|
||||||
|
|
||||||
#title-seperator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
/* margin-left: 10%; */
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
/* max-width: 1250px; */
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -108,14 +99,16 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left,#prev {
|
#left,
|
||||||
|
#prev {
|
||||||
left: 40px;
|
left: 40px;
|
||||||
padding-right:80px;
|
padding-right: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#right,#next {
|
#right,
|
||||||
|
#next {
|
||||||
right: 40px;
|
right: 40px;
|
||||||
padding-left:80px;
|
padding-left: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
@@ -148,17 +141,13 @@ body {
|
|||||||
#sidebar {
|
#sidebar {
|
||||||
background: #6b6b6b;
|
background: #6b6b6b;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
/* left: -260px; */
|
|
||||||
/* -webkit-transform: translate(-260px, 0);
|
|
||||||
-moz-transform: translate(-260px, 0); */
|
|
||||||
top: 0;
|
top: 0;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-transition: -webkit-transform .5s;
|
-webkit-transition: -webkit-transform 0.5s;
|
||||||
-moz-transition: -moz-transform .5s;
|
-moz-transition: -moz-transform 0.5s;
|
||||||
-ms-transition: -moz-transform .5s;
|
-ms-transition: -moz-transform 0.5s;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,10 +181,10 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 13px 0;
|
padding: 13px 0;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
-moz-box-shadow: 0px 1px 3px rgba(0,0,0,.6);
|
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||||
-webkit-box-shadow: 0px 1px 3px rgba(0,0,0,.6);
|
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||||
-ms-box-shadow: 0px 1px 3px rgba(0,0,0,.6);
|
-ms-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||||
box-shadow: 0px 1px 3px rgba(0,0,0,.6);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
#opener {
|
#opener {
|
||||||
@@ -203,19 +192,13 @@ body {
|
|||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #opener #slider {
|
|
||||||
width: 25px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
#metainfo {
|
#metainfo {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#title-controls {
|
#title-controls { float: right; }
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#panels a {
|
#panels a {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -227,22 +210,17 @@ body {
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#panels a::before {
|
#panels a::before { visibility: visible; }
|
||||||
visibility: visible;
|
#panels a:hover { color: #aaa; }
|
||||||
}
|
|
||||||
|
|
||||||
#panels a:hover {
|
|
||||||
color: #AAA;
|
|
||||||
}
|
|
||||||
|
|
||||||
#panels a:active {
|
#panels a:active {
|
||||||
color: #AAA;
|
color: #aaa;
|
||||||
margin: 1px 0 -1px 6px;
|
margin: 1px 0 -1px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#panels a.active,
|
#panels a.active,
|
||||||
#panels a.active:hover {
|
#panels a.active:hover {
|
||||||
color: #AAA;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
#searchBox {
|
#searchBox {
|
||||||
@@ -250,28 +228,11 @@ body {
|
|||||||
float: left;
|
float: left;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
/*
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #9b9b9b;
|
|
||||||
float: left;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-top: -5px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
color: #000;
|
|
||||||
border: none;
|
|
||||||
outline: none; */
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input::-webkit-input-placeholder {
|
input::-webkit-input-placeholder { color: #454545; }
|
||||||
color: #454545;
|
input:-moz-placeholder { color: #454545; }
|
||||||
}
|
input:-ms-placeholder { color: #454545; }
|
||||||
input:-moz-placeholder {
|
|
||||||
color: #454545;
|
|
||||||
}
|
|
||||||
input:-ms-placeholder {
|
|
||||||
color: #454545;
|
|
||||||
}
|
|
||||||
|
|
||||||
#divider {
|
#divider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -307,13 +268,11 @@ input:-ms-placeholder {
|
|||||||
width: 25%;
|
width: 25%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
-webkit-transition: visibility 0 ease .5s;
|
-webkit-transition: visibility 0 ease 0.5s;
|
||||||
-moz-transition: visibility 0 ease .5s;
|
-moz-transition: visibility 0 ease 0.5s;
|
||||||
-ms-transition: visibility 0 ease .5s;
|
-ms-transition: visibility 0 ease 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#sidebar.open #tocView,
|
#sidebar.open #tocView,
|
||||||
#sidebar.open #bookmarksView {
|
#sidebar.open #bookmarksView {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -351,7 +310,7 @@ input:-ms-placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list_item a {
|
.list_item a {
|
||||||
color: #AAA;
|
color: #aaa;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +319,7 @@ input:-ms-placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list_item a.section {
|
.list_item a.section {
|
||||||
font-size: .8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list_item.currentChapter > a,
|
.list_item.currentChapter > a,
|
||||||
@@ -370,7 +329,7 @@ input:-ms-placeholder {
|
|||||||
|
|
||||||
/* #tocView li.openChapter > a, */
|
/* #tocView li.openChapter > a, */
|
||||||
.list_item a:hover {
|
.list_item a:hover {
|
||||||
color: #E2E2E2;
|
color: #e2e2e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list_item ul {
|
.list_item ul {
|
||||||
@@ -431,7 +390,7 @@ input:-ms-placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#searchResults a {
|
#searchResults a {
|
||||||
color: #AAA;
|
color: #aaa;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,11 +406,11 @@ input:-ms-placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#searchResults li > p {
|
#searchResults li > p {
|
||||||
color: #AAA;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
#searchResults li a:hover {
|
#searchResults li a:hover {
|
||||||
color: #E2E2E2;
|
color: #e2e2e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#searchView.shown {
|
#searchView.shown {
|
||||||
@@ -496,7 +455,7 @@ input:-ms-placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#note-text[disabled], #note-text[disabled="disabled"]{
|
#note-text[disabled], #note-text[disabled="disabled"]{
|
||||||
opacity: .5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#note-anchor {
|
#note-anchor {
|
||||||
@@ -505,30 +464,30 @@ input:-ms-placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#settingsPanel {
|
#settingsPanel {
|
||||||
display:none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settingsPanel h3 {
|
#settingsPanel h3 {
|
||||||
color:#f1f1f1;
|
color: #f1f1f1;
|
||||||
font-family:Georgia, "Times New Roman", Times, serif;
|
font-family: Georgia, "Times New Roman", Times, serif;
|
||||||
margin-bottom:10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settingsPanel ul {
|
#settingsPanel ul {
|
||||||
margin-top:60px;
|
margin-top: 60px;
|
||||||
list-style-type:none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settingsPanel li {
|
#settingsPanel li {
|
||||||
font-size:1em;
|
font-size: 1em;
|
||||||
color:#f1f1f1;
|
color: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settingsPanel .xsmall { font-size:x-small; }
|
#settingsPanel .xsmall { font-size: x-small; }
|
||||||
#settingsPanel .small { font-size:small; }
|
#settingsPanel .small { font-size: small; }
|
||||||
#settingsPanel .medium { font-size:medium; }
|
#settingsPanel .medium { font-size: medium; }
|
||||||
#settingsPanel .large { font-size:large; }
|
#settingsPanel .large { font-size: large; }
|
||||||
#settingsPanel .xlarge { font-size:x-large; }
|
#settingsPanel .xlarge { font-size: x-large; }
|
||||||
|
|
||||||
.highlight { background-color: yellow }
|
.highlight { background-color: yellow }
|
||||||
|
|
||||||
@@ -556,7 +515,7 @@ input:-ms-placeholder {
|
|||||||
left: 0;
|
left: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background: rgba(255,255,255,0.8);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
-webkit-transition: all 0.3s;
|
-webkit-transition: all 0.3s;
|
||||||
-moz-transition: all 0.3s;
|
-moz-transition: all 0.3s;
|
||||||
-ms-transition: all 0.3s;
|
-ms-transition: all 0.3s;
|
||||||
@@ -589,7 +548,7 @@ input:-ms-placeholder {
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
background: rgba(0,0,0,0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 3px 3px 0 0;
|
border-radius: 3px 3px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,23 +782,18 @@ and (orientation : landscape)
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
speak: none;
|
speak: none;
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
margin-right: .2em;
|
margin-right: 0.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* opacity: .8; */
|
|
||||||
|
|
||||||
/* For safety - reset parent styles, that can break glyph codes*/
|
/* For safety - reset parent styles, that can break glyph codes*/
|
||||||
font-variant: normal;
|
font-variant: normal;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
|
||||||
/* you can be more comfortable with increased icons size */
|
/* you can be more comfortable with increased icons size */
|
||||||
font-size: 112%;
|
font-size: 112%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.icon-search:before { content: '\e807'; } /* '' */
|
.icon-search:before { content: '\e807'; } /* '' */
|
||||||
.icon-resize-full-1:before { content: '\e804'; } /* '' */
|
.icon-resize-full-1:before { content: '\e804'; } /* '' */
|
||||||
.icon-cancel-circled2:before { content: '\e80f'; } /* '' */
|
.icon-cancel-circled2:before { content: '\e80f'; } /* '' */
|
||||||
|
|||||||
+11
-14
@@ -1,4 +1,5 @@
|
|||||||
/* http://davidwalsh.name/css-tooltips */
|
/* http://davidwalsh.name/css-tooltips */
|
||||||
|
|
||||||
/* base CSS element */
|
/* base CSS element */
|
||||||
.popup {
|
.popup {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
@@ -9,10 +10,8 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
|
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* below */
|
/* below */
|
||||||
.popup:before {
|
.popup::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-bottom: 10px solid #eee;
|
border-bottom: 10px solid #eee;
|
||||||
@@ -51,7 +50,7 @@
|
|||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup:after {
|
.popup::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-bottom: 9px solid #eee;
|
border-bottom: 9px solid #eee;
|
||||||
@@ -64,33 +63,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* above */
|
/* above */
|
||||||
.popup.above:before {
|
.popup.above::before {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-top: 10px solid #eee;
|
border-top: 10px solid #eee;
|
||||||
border-top-color: rgba(0, 0, 0, 0.2);
|
border-top-color: rgba(0, 0, 0, 0.2);
|
||||||
top: 100%;
|
top: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup.above:after {
|
.popup.above::after {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-top: 9px solid #eee;
|
border-top: 9px solid #eee;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup.left:before,
|
.popup.left::before,
|
||||||
.popup.left:after
|
.popup.left::after {
|
||||||
{
|
|
||||||
left: 20px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup.right:before,
|
.popup.right::before,
|
||||||
.popup.right:after
|
.popup.right::after {
|
||||||
{
|
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup.show,
|
||||||
.popup.show, .popup.on {
|
.popup.on {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
+219
-73
@@ -1,5 +1,20 @@
|
|||||||
|
|
||||||
.tooltip.bottom .tooltip-inner{font-size:13px;font-family:Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;padding:3px 10px;border-radius:4px;background-color:#fff;-webkit-box-shadow:0 4px 10px 0 rgba(0,0,0,.35);box-shadow:0 4px 10px 0 rgba(0,0,0,.35);opacity:1;white-space:nowrap;margin-top:-16px!important;line-height:1.71428571;color:#ddd}
|
.tooltip.bottom .tooltip-inner {
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
-webkit-box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.35);
|
||||||
|
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.35);
|
||||||
|
opacity: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-top: -16px !important;
|
||||||
|
line-height: 1.71428571;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Grand Hotel';
|
font-family: 'Grand Hotel';
|
||||||
@@ -12,76 +27,204 @@ html.http-error {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.http-error body {
|
.http-error body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.http-error body > div {
|
.http-error body > div {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
|
body {
|
||||||
body { margin-bottom: 40px;}
|
background: #f2f2f2;
|
||||||
a{color: #45b29d}a:hover{color: #444;}
|
margin-bottom: 40px;
|
||||||
.navigation .nav-head{text-transform:uppercase;color:#999;margin:20px 0}.navigation .nav-head:nth-child(1n+2){border-top:1px solid #ccc;padding-top:20px}
|
|
||||||
.navigation li a{color:#444;text-decoration:none;display:block;padding:10px}.navigation li a:hover{background:rgba(153,153,153,0.4);border-radius:5px}
|
|
||||||
.navigation li a span{margin-right:10px}
|
|
||||||
.navigation .create-shelf{margin:30px 0;font-size:12px;text-align:center}.navigation .create-shelf a{color:#fff;background:#45b29d;padding:10px 20px;border-radius:5px;text-decoration:none}
|
|
||||||
.container-fluid img{display:block;max-width:100%;height:auto}
|
|
||||||
.container-fluid .discover{margin-bottom:50px}
|
|
||||||
.container-fluid .new-books{border-top:1px solid #ccc}.container-fluid .new-books h2{margin:50px 0 0 0}
|
|
||||||
.container-fluid .book{margin-top:20px}.container-fluid .book .cover{height:225px;position:relative}.container-fluid .book .cover img{border:1px solid #fff;/*border-radius:7px;*/box-sizeing:border-box;height:100%;bottom:0;position:absolute;-webkit-box-shadow: 0 5px 8px -6px #777;-moz-box-shadow: 0 5px 8px -6px #777;box-shadow: 0 5px 8px -6px #777;}
|
|
||||||
.container-fluid .book .meta{margin-top:10px}.container-fluid .book .meta p{margin:0}
|
|
||||||
.container-fluid .book .meta .title{font-weight:bold;font-size:15px;color:#444}
|
|
||||||
.container-fluid .book .meta .author{font-size:12px;color:#999}
|
|
||||||
.container-fluid .book .meta .rating{margin-top:5px}.rating .glyphicon-star{color:#999}.rating .glyphicon-star.good{color:#45b29d}
|
|
||||||
|
|
||||||
.container-fluid .author .author-hidden, .container-fluid .author .author-hidden-divider {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand{font-family: 'Grand Hotel', cursive; font-size: 35px; color: #45b29d !important;}
|
body h2 {
|
||||||
.more-stuff{margin-top: 20px; padding-top: 20px; border-top: 1px solid #ccc}
|
font-weight: normal;
|
||||||
.more-stuff>li{margin-bottom: 10px;}
|
color:#444;
|
||||||
.navbar-collapse.in .navbar-nav{margin: 0;}
|
}
|
||||||
span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: text-top;}
|
|
||||||
.book-meta {padding-bottom: 20px;}
|
a { color: #45b29d; }
|
||||||
.book-meta .tags a {display: inline;}
|
|
||||||
.book-meta .identifiers a {display: inline;}
|
.navigation .nav-head {
|
||||||
.container-fluid .single .cover img {
|
text-transform: uppercase;
|
||||||
|
color: #999;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation .nav-head:nth-child(1n+2) {
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.navigation li a {
|
||||||
|
color: #444;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation li a:hover {
|
||||||
|
background: rgba(153, 153, 153, 0.4);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation li a span { margin-right: 10px; }
|
||||||
|
|
||||||
|
.navigation .create-shelf {
|
||||||
|
margin: 30px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation .create-shelf a {
|
||||||
|
color: #fff;
|
||||||
|
background: #45b29d;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid .discover{ margin-bottom: 50px; }
|
||||||
|
.container-fluid .new-books { border-top: 1px solid #ccc; }
|
||||||
|
.container-fluid .new-books h2 { margin: 50px 0 0 0; }
|
||||||
|
.container-fluid .book { margin-top: 20px; }
|
||||||
|
|
||||||
|
.container-fluid .book .cover {
|
||||||
|
height: 225px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid .book .cover img {
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
/*border-radius: 7px;*/
|
box-sizing: border-box;
|
||||||
box-sizeing: border-box;
|
height: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
-webkit-box-shadow: 0 5px 8px -6px #777;
|
-webkit-box-shadow: 0 5px 8px -6px #777;
|
||||||
-moz-box-shadow: 0 5px 8px -6px #777;
|
-moz-box-shadow: 0 5px 8px -6px #777;
|
||||||
box-shadow: 0 5px 8px -6px #777;
|
box-shadow: 0 5px 8px -6px #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-default .navbar-toggle .icon-bar {background-color: #000;}
|
.container-fluid .book .meta { margin-top: 10px; }
|
||||||
.navbar-default .navbar-toggle {border-color: #000;}
|
.container-fluid .book .meta p { margin: 0; }
|
||||||
.cover { margin-bottom: 10px;}
|
|
||||||
|
.container-fluid .book .meta .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid .book .meta .author {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid .book .meta .rating { margin-top: 5px; }
|
||||||
|
.rating .glyphicon-star-empty { color: #444; }
|
||||||
|
.rating .glyphicon-star.good { color: #444; }
|
||||||
|
.rating-clear .glyphicon-remove { color: #333 }
|
||||||
|
|
||||||
|
.container-fluid .author .author-hidden, .container-fluid .author .author-hidden-divider { display: none; }
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-family: 'Grand Hotel', cursive;
|
||||||
|
font-size: 35px;
|
||||||
|
color: #45b29d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-stuff {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-stuff>li { margin-bottom: 10px; }
|
||||||
|
.navbar-collapse.in .navbar-nav { margin: 0; }
|
||||||
|
|
||||||
|
span.glyphicon.glyphicon-tags {
|
||||||
|
padding-right: 5px;
|
||||||
|
color: #999;
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-meta { padding-bottom: 20px; }
|
||||||
|
.book-meta .tags a { display: inline; }
|
||||||
|
.book-meta .identifiers a { display: inline; }
|
||||||
|
|
||||||
|
.container-fluid .single .cover img {
|
||||||
|
border: 1px solid #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-box-shadow: 0 5px 8px -6px #777;
|
||||||
|
-moz-box-shadow: 0 5px 8px -6px #777;
|
||||||
|
box-shadow: 0 5px 8px -6px #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-default .navbar-toggle .icon-bar {background-color: #000; }
|
||||||
|
.navbar-default .navbar-toggle {border-color: #000; }
|
||||||
|
.cover { margin-bottom: 10px; }
|
||||||
|
.cover .badge{
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
background-color: #777;
|
||||||
|
}
|
||||||
.cover-height { max-height: 100px;}
|
.cover-height { max-height: 100px;}
|
||||||
|
|
||||||
|
.col-sm-2 a .cover-small {
|
||||||
|
margin: 5px;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-file {position: relative; overflow: hidden;}
|
.btn-file {position: relative; overflow: hidden;}
|
||||||
.btn-file input[type=file] {position: absolute; top: 0; right: 0; min-width: 100%; min-height: 100%; font-size: 100px; text-align: right; filter: alpha(opacity=0); opacity: 0; outline: none; background: white; cursor: inherit; display: block;}
|
|
||||||
|
.btn-file input[type=file] {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
font-size: 100px;
|
||||||
|
text-align: right;
|
||||||
|
filter: alpha(opacity=0);
|
||||||
|
opacity: 0;
|
||||||
|
outline: none;
|
||||||
|
background: white;
|
||||||
|
cursor: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-toolbar .btn,.discover .btn { margin-bottom: 5px; }
|
.btn-toolbar .btn,.discover .btn { margin-bottom: 5px; }
|
||||||
.button-link {color:#fff;}
|
.button-link {color: #fff; }
|
||||||
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; }
|
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; }
|
||||||
.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; }
|
.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; }
|
||||||
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left:0px; }
|
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0px; }
|
||||||
.panel-body {background-color: #f5f5f5;}
|
.panel-body {background-color: #f5f5f5; }
|
||||||
.spinner {margin:0 41%;}
|
.spinner {margin: 0 41%; }
|
||||||
.spinner2 {margin:0 41%;}
|
.spinner2 {margin: 0 41%; }
|
||||||
|
.intend-form { margin-left:20px; }
|
||||||
|
table .bg-dark-danger {background-color: #d9534f; color: #fff; }
|
||||||
|
table .bg-dark-danger a {color: #fff; }
|
||||||
|
table .bg-dark-danger:hover {background-color: #c9302c; }
|
||||||
|
table .bg-primary:hover {background-color: #1C5484; }
|
||||||
|
table .bg-primary a {color: #fff; }
|
||||||
.block-label {display: block;}
|
.block-label {display: block;}
|
||||||
.fake-input {position: absolute; pointer-events: none; top: 0;}
|
.fake-input {position: absolute; pointer-events: none; top: 0; }
|
||||||
|
|
||||||
input.pill { position: absolute; opacity: 0; }
|
input.pill { position: absolute; opacity: 0; }
|
||||||
|
|
||||||
input.pill + label {
|
input.pill + label {
|
||||||
border: 2px solid #45b29d;
|
border: 2px solid #45b29d;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
@@ -93,58 +236,60 @@ input.pill + label {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.pill:checked + label {
|
input.pill:checked + label {
|
||||||
background-color: #45b29d;
|
background-color: #45b29d;
|
||||||
border-color: #fff;
|
border-color: #fff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
input.pill:not(:checked) + label .glyphicon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-bio img {margin: 0 1em 1em 0;}
|
input.pill:not(:checked) + label .glyphicon { display: none; }
|
||||||
.author-link {display: inline-block; margin-top: 10px; width: 100px;}
|
|
||||||
.author-link img {display: block; height: 100%;}
|
|
||||||
|
|
||||||
#remove-from-shelves .btn,
|
.author-bio img { margin: 0 1em 1em 0; }
|
||||||
#shelf-action-errors {
|
.author-link { display: inline-block; margin-top: 10px; width: 100px; }
|
||||||
margin-left: 5px;
|
.author-link img { display: block; height: 100%; }
|
||||||
}
|
#remove-from-shelves .btn, #shelf-action-errors { margin-left: 5px; }
|
||||||
|
|
||||||
.tags_click, .serie_click, .language_click {margin-right: 5px;}
|
.tags_click, .serie_click, .language_click { margin-right: 5px; }
|
||||||
|
|
||||||
#meta-info {
|
#meta-info {
|
||||||
height:600px;
|
height: 600px;
|
||||||
overflow-y:scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
.media-list {
|
|
||||||
padding-right:15px;
|
.media-list { padding-right: 15px; }
|
||||||
|
.media-body p { text-align: justify; }
|
||||||
|
|
||||||
|
#meta-info img {
|
||||||
|
max-height: 150px;
|
||||||
|
max-width: 100px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.media-body p {
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
#meta-info img { max-height: 150px; max-width: 100px; cursor: pointer; }
|
|
||||||
|
|
||||||
.padded-bottom { margin-bottom: 15px; }
|
.padded-bottom { margin-bottom: 15px; }
|
||||||
|
.upload-format-input-text { display: initial; }
|
||||||
|
#btn-upload-format { display: none; }
|
||||||
|
.upload-cover-input-text { display: initial; }
|
||||||
|
#btn-upload-cover { display: none; }
|
||||||
|
.panel-title > a { text-decoration: none; }
|
||||||
|
.editable-buttons {
|
||||||
|
display:inline-block;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-format-input-text {display: initial;}
|
.editable-input { display:inline-block; }
|
||||||
#btn-upload-format {display: none;}
|
|
||||||
|
|
||||||
.upload-cover-input-text {display: initial;}
|
.editable-cancel {
|
||||||
#btn-upload-cover {display: none;}
|
margin-bottom: 0px !important;
|
||||||
|
margin-left: 7px !important;
|
||||||
.panel-title > a { text-decoration: none;}
|
}
|
||||||
|
|
||||||
.editable-buttons { display:inline-block; margin-left: 7px ;}
|
|
||||||
.editable-input { display:inline-block;}
|
|
||||||
.editable-cancel { margin-bottom: 0px !important; margin-left: 7px !important;}
|
|
||||||
.editable-submit { margin-bottom: 0px !important;}
|
|
||||||
|
|
||||||
|
.editable-submit { margin-bottom: 0px !important; }
|
||||||
.filterheader { margin-bottom: 20px; }
|
.filterheader { margin-bottom: 20px; }
|
||||||
|
.errorlink { margin-top: 20px; }
|
||||||
|
|
||||||
.errorlink {margin-top: 20px;}
|
|
||||||
.modal-body .comments {
|
.modal-body .comments {
|
||||||
max-height:300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,3 +303,4 @@ div.log {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ function alphanumCase(a, b) {
|
|||||||
|
|
||||||
while (i = (j = t.charAt(x++)).charCodeAt(0)) {
|
while (i = (j = t.charAt(x++)).charCodeAt(0)) {
|
||||||
var m = (i === 46 || (i >= 48 && i <= 57));
|
var m = (i === 46 || (i >= 48 && i <= 57));
|
||||||
if (m !== n) {
|
// Compare has to be with != otherwise fails
|
||||||
|
if (m != n) {
|
||||||
tz[++y] = "";
|
tz[++y] = "";
|
||||||
n = m;
|
n = m;
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,8 @@ function alphanumCase(a, b) {
|
|||||||
for (var x = 0; aa[x] && bb[x]; x++) {
|
for (var x = 0; aa[x] && bb[x]; x++) {
|
||||||
if (aa[x] !== bb[x]) {
|
if (aa[x] !== bb[x]) {
|
||||||
var c = Number(aa[x]), d = Number(bb[x]);
|
var c = Number(aa[x]), d = Number(bb[x]);
|
||||||
if (c === aa[x] && d === bb[x]) {
|
// Compare has to be with == otherwise fails
|
||||||
|
if (c == aa[x] && d == bb[x]) {
|
||||||
return c - d;
|
return c - d;
|
||||||
} else {
|
} else {
|
||||||
return (aa[x] > bb[x]) ? 1 : -1;
|
return (aa[x] > bb[x]) ? 1 : -1;
|
||||||
|
|||||||
+437
-389
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,14 @@ $("#have_read_cb").on("change", function() {
|
|||||||
$(this).closest("form").submit();
|
$(this).closest("form").submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$("#archived_form").ajaxForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#archived_cb").on("change", function() {
|
||||||
|
$(this).closest("form").submit();
|
||||||
|
});
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var templates = {
|
var templates = {
|
||||||
add: _.template(
|
add: _.template(
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
* Copyright (C) 2018 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var $list = $("#list").isotope({
|
||||||
|
itemSelector: ".book",
|
||||||
|
layoutMode: "fitRows",
|
||||||
|
getSortData: {
|
||||||
|
title: ".title",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#desc").click(function() {
|
||||||
|
$list.isotope({
|
||||||
|
sortBy: "name",
|
||||||
|
sortAscending: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#asc").click(function() {
|
||||||
|
$list.isotope({
|
||||||
|
sortBy: "name",
|
||||||
|
sortAscending: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#all").click(function() {
|
||||||
|
// go through all elements and make them visible
|
||||||
|
$list.isotope({ filter: function() {
|
||||||
|
return true;
|
||||||
|
} })
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".char").click(function() {
|
||||||
|
var character = this.innerText;
|
||||||
|
$list.isotope({ filter: function() {
|
||||||
|
return this.attributes["data-id"].value.charAt(0).toUpperCase() == character;
|
||||||
|
} })
|
||||||
|
});
|
||||||
@@ -45,7 +45,7 @@ $("#sort_name").click(function() {
|
|||||||
});*/
|
});*/
|
||||||
// Find count of middle element
|
// Find count of middle element
|
||||||
if (count > 20) {
|
if (count > 20) {
|
||||||
var middle = parseInt(count / 2) + (count % 2);
|
var middle = parseInt(count / 2, 10) + (count % 2);
|
||||||
// search for the middle of all visibe elements
|
// search for the middle of all visibe elements
|
||||||
$(".row").each(function() {
|
$(".row").each(function() {
|
||||||
index++;
|
index++;
|
||||||
@@ -146,7 +146,7 @@ $("#all").click(function() {
|
|||||||
// Find count of middle element
|
// Find count of middle element
|
||||||
var listItems = $("#list").children(".row");
|
var listItems = $("#list").children(".row");
|
||||||
var listlength = listItems.length;
|
var listlength = listItems.length;
|
||||||
var middle = parseInt(listlength / 2) + (listlength % 2);
|
var middle = parseInt(listlength / 2, 10) + (listlength % 2);
|
||||||
// go through all elements and make them visible
|
// go through all elements and make them visible
|
||||||
listItems.each(function() {
|
listItems.each(function() {
|
||||||
$(this).show();
|
$(this).show();
|
||||||
@@ -178,7 +178,7 @@ $(".char").click(function() {
|
|||||||
});
|
});
|
||||||
if (count > 20) {
|
if (count > 20) {
|
||||||
// Find count of middle element
|
// Find count of middle element
|
||||||
var middle = parseInt(count / 2) + (count % 2);
|
var middle = parseInt(count / 2, 10) + (count % 2);
|
||||||
// search for the middle of all visibe elements
|
// search for the middle of all visibe elements
|
||||||
$(".row").each(function() {
|
$(".row").each(function() {
|
||||||
index++;
|
index++;
|
||||||
|
|||||||
+131
-34
@@ -15,23 +15,29 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
* Get Metadata from Douban Books api and Google Books api
|
* Get Metadata from Douban Books api and Google Books api and ComicVine
|
||||||
* Google Books api document: https://developers.google.com/books/docs/v1/using
|
* Google Books api document: https://developers.google.com/books/docs/v1/using
|
||||||
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
|
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
|
||||||
|
* ComicVine api document: https://comicvine.gamespot.com/api/documentation
|
||||||
*/
|
*/
|
||||||
/* global _, i18nMsg, tinymce */
|
/* global _, i18nMsg, tinymce */
|
||||||
var dbResults = [];
|
var dbResults = [];
|
||||||
var ggResults = [];
|
var ggResults = [];
|
||||||
|
var cvResults = [];
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
var msg = i18nMsg;
|
var msg = i18nMsg;
|
||||||
var douban = "https://api.douban.com";
|
var douban = "https://api.douban.com";
|
||||||
var dbSearch = "/v2/book/search";
|
var dbSearch = "/v2/book/search";
|
||||||
var dbDone = true;
|
var dbDone = 0;
|
||||||
|
|
||||||
var google = "https://www.googleapis.com";
|
var google = "https://www.googleapis.com";
|
||||||
var ggSearch = "/books/v1/volumes";
|
var ggSearch = "/books/v1/volumes";
|
||||||
var ggDone = false;
|
var ggDone = 0;
|
||||||
|
|
||||||
|
var comicvine = "https://comicvine.gamespot.com";
|
||||||
|
var cvSearch = "/api/search/";
|
||||||
|
var cvDone = 0;
|
||||||
|
|
||||||
var showFlag = 0;
|
var showFlag = 0;
|
||||||
|
|
||||||
@@ -48,16 +54,17 @@ $(function () {
|
|||||||
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
|
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#bookAuthor").val(book.authors);
|
var ampSeparatedAuthors = (book.authors || []).join(" & ");
|
||||||
|
$("#bookAuthor").val(ampSeparatedAuthors);
|
||||||
$("#book_title").val(book.title);
|
$("#book_title").val(book.title);
|
||||||
$("#tags").val(uniqueTags.join(","));
|
$("#tags").val(uniqueTags.join(","));
|
||||||
$("#rating").data("rating").setValue(Math.round(book.rating));
|
$("#rating").data("rating").setValue(Math.round(book.rating));
|
||||||
$(".cover img").attr("src", book.cover);
|
$(".cover img").attr("src", book.cover);
|
||||||
$("#cover_url").val(book.cover);
|
$("#cover_url").val(book.cover);
|
||||||
$("#pubdate").val(book.publishedDate);
|
$("#pubdate").val(book.publishedDate);
|
||||||
$("#publisher").val(book.publisher)
|
$("#publisher").val(book.publisher);
|
||||||
if (book.series != undefined) {
|
if (typeof book.series !== "undefined") {
|
||||||
$("#series").val(book.series)
|
$("#series").val(book.series);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,25 +73,30 @@ $(function () {
|
|||||||
if (showFlag === 1) {
|
if (showFlag === 1) {
|
||||||
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
||||||
}
|
}
|
||||||
if (!ggDone && !dbDone) {
|
if ((ggDone === 3 || (ggDone === 1 && ggResults.length === 0)) &&
|
||||||
|
(dbDone === 3 || (dbDone === 1 && dbResults.length === 0)) &&
|
||||||
|
(cvDone === 3 || (cvDone === 1 && cvResults.length === 0))) {
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
function formatDate (date) {
|
function formatDate (date) {
|
||||||
var d = new Date(date),
|
var d = new Date(date),
|
||||||
month = '' + (d.getMonth() + 1),
|
month = "" + (d.getMonth() + 1),
|
||||||
day = '' + d.getDate(),
|
day = "" + d.getDate(),
|
||||||
year = d.getFullYear();
|
year = d.getFullYear();
|
||||||
|
|
||||||
if (month.length < 2)
|
if (month.length < 2) {
|
||||||
month = '0' + month;
|
month = "0" + month;
|
||||||
if (day.length < 2)
|
}
|
||||||
day = '0' + day;
|
if (day.length < 2) {
|
||||||
|
day = "0" + day;
|
||||||
return [year, month, day].join('-');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ggDone && ggResults.length > 0) {
|
return [year, month, day].join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ggResults.length > 0) {
|
||||||
|
if (ggDone < 2) {
|
||||||
ggResults.forEach(function(result) {
|
ggResults.forEach(function(result) {
|
||||||
var book = {
|
var book = {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
@@ -112,19 +124,25 @@ $(function () {
|
|||||||
|
|
||||||
$("#book-list").append($book);
|
$("#book-list").append($book);
|
||||||
});
|
});
|
||||||
ggDone = false;
|
ggDone = 2;
|
||||||
|
} else {
|
||||||
|
ggDone = 3;
|
||||||
}
|
}
|
||||||
if (dbDone && dbResults.length > 0) {
|
|
||||||
dbResults.forEach(function(result) {
|
|
||||||
if (result.series){
|
|
||||||
var series_title = result.series.title
|
|
||||||
}
|
}
|
||||||
var date_fomers = result.pubdate.split("-")
|
|
||||||
var publishedYear = parseInt(date_fomers[0])
|
|
||||||
var publishedMonth = parseInt(date_fomers[1])
|
|
||||||
var publishedDate = new Date(publishedYear, publishedMonth-1, 1)
|
|
||||||
|
|
||||||
publishedDate = formatDate(publishedDate)
|
if (dbResults.length > 0) {
|
||||||
|
if (dbDone < 2) {
|
||||||
|
dbResults.forEach(function(result) {
|
||||||
|
var seriesTitle = "";
|
||||||
|
if (result.series) {
|
||||||
|
seriesTitle = result.series.title;
|
||||||
|
}
|
||||||
|
var dateFomers = result.pubdate.split("-");
|
||||||
|
var publishedYear = parseInt(dateFomers[0]);
|
||||||
|
var publishedMonth = parseInt(dateFomers[1]);
|
||||||
|
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
|
||||||
|
|
||||||
|
publishedDate = formatDate(publishedDate);
|
||||||
|
|
||||||
var book = {
|
var book = {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
@@ -137,7 +155,7 @@ $(function () {
|
|||||||
return tag.title.toLowerCase().replace(/,/g, "_");
|
return tag.title.toLowerCase().replace(/,/g, "_");
|
||||||
}),
|
}),
|
||||||
rating: result.rating.average || 0,
|
rating: result.rating.average || 0,
|
||||||
series: series_title || "",
|
series: seriesTitle || "",
|
||||||
cover: result.image,
|
cover: result.image,
|
||||||
url: "https://book.douban.com/subject/" + result.id,
|
url: "https://book.douban.com/subject/" + result.id,
|
||||||
source: {
|
source: {
|
||||||
@@ -158,7 +176,60 @@ $(function () {
|
|||||||
|
|
||||||
$("#book-list").append($book);
|
$("#book-list").append($book);
|
||||||
});
|
});
|
||||||
dbDone = false;
|
dbDone = 2;
|
||||||
|
} else {
|
||||||
|
dbDone = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cvResults.length > 0) {
|
||||||
|
if (cvDone < 2) {
|
||||||
|
cvResults.forEach(function(result) {
|
||||||
|
var seriesTitle = "";
|
||||||
|
if (result.volume.name) {
|
||||||
|
seriesTitle = result.volume.name;
|
||||||
|
}
|
||||||
|
var dateFomers = "";
|
||||||
|
if (result.store_date) {
|
||||||
|
dateFomers = result.store_date.split("-");
|
||||||
|
} else {
|
||||||
|
dateFomers = result.date_added.split("-");
|
||||||
|
}
|
||||||
|
var publishedYear = parseInt(dateFomers[0]);
|
||||||
|
var publishedMonth = parseInt(dateFomers[1]);
|
||||||
|
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
|
||||||
|
|
||||||
|
publishedDate = formatDate(publishedDate);
|
||||||
|
|
||||||
|
var book = {
|
||||||
|
id: result.id,
|
||||||
|
title: seriesTitle + " #" + ("00" + result.issue_number).slice(-3) + " - " + result.name,
|
||||||
|
authors: result.author || [],
|
||||||
|
description: result.description,
|
||||||
|
publisher: "",
|
||||||
|
publishedDate: publishedDate || "",
|
||||||
|
tags: ["Comics", seriesTitle],
|
||||||
|
rating: 0,
|
||||||
|
series: seriesTitle || "",
|
||||||
|
cover: result.image.original_url,
|
||||||
|
url: result.site_detail_url,
|
||||||
|
source: {
|
||||||
|
id: "comicvine",
|
||||||
|
description: "ComicVine Books",
|
||||||
|
url: "https://comicvine.gamespot.com/"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var $book = $(templates.bookResult(book));
|
||||||
|
$book.find("img").on("click", function () {
|
||||||
|
populateForm(book);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#book-list").append($book);
|
||||||
|
});
|
||||||
|
cvDone = 2;
|
||||||
|
} else {
|
||||||
|
cvDone = 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,11 +242,10 @@ $(function () {
|
|||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
if ("items" in data) {
|
if ("items" in data) {
|
||||||
ggResults = data.items;
|
ggResults = data.items;
|
||||||
ggDone = true;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
complete: function complete() {
|
complete: function complete() {
|
||||||
ggDone = true;
|
ggDone = 1;
|
||||||
showResult();
|
showResult();
|
||||||
$("#show-google").trigger("change");
|
$("#show-google").trigger("change");
|
||||||
}
|
}
|
||||||
@@ -183,7 +253,7 @@ $(function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dbSearchBook (title) {
|
function dbSearchBook (title) {
|
||||||
apikey="0df993c66c0c636e29ecbb5344252a4a"
|
var apikey = "0df993c66c0c636e29ecbb5344252a4a";
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10",
|
url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10",
|
||||||
type: "GET",
|
type: "GET",
|
||||||
@@ -193,22 +263,49 @@ $(function () {
|
|||||||
dbResults = data.books;
|
dbResults = data.books;
|
||||||
},
|
},
|
||||||
error: function error() {
|
error: function error() {
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>"+ $("#meta-info")[0].innerHTML)
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
||||||
},
|
},
|
||||||
complete: function complete() {
|
complete: function complete() {
|
||||||
dbDone = true;
|
dbDone = 1;
|
||||||
showResult();
|
showResult();
|
||||||
$("#show-douban").trigger("change");
|
$("#show-douban").trigger("change");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cvSearchBook (title) {
|
||||||
|
var apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6";
|
||||||
|
title = encodeURIComponent(title);
|
||||||
|
$.ajax({
|
||||||
|
url: comicvine + cvSearch + "?api_key=" + apikey + "&resources=issue&query=" + title + "&sort=name:desc&format=jsonp",
|
||||||
|
type: "GET",
|
||||||
|
dataType: "jsonp",
|
||||||
|
jsonp: "json_callback",
|
||||||
|
success: function success(data) {
|
||||||
|
cvResults = data.results;
|
||||||
|
},
|
||||||
|
error: function error() {
|
||||||
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
||||||
|
},
|
||||||
|
complete: function complete() {
|
||||||
|
cvDone = 1;
|
||||||
|
showResult();
|
||||||
|
$("#show-comics").trigger("change");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function doSearch (keyword) {
|
function doSearch (keyword) {
|
||||||
showFlag = 0;
|
showFlag = 0;
|
||||||
|
dbDone = ggDone = cvDone = 0;
|
||||||
|
dbResults = [];
|
||||||
|
ggResults = [];
|
||||||
|
cvResults = [];
|
||||||
$("#meta-info").text(msg.loading);
|
$("#meta-info").text(msg.loading);
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
dbSearchBook(keyword);
|
dbSearchBook(keyword);
|
||||||
ggSearchBook(keyword);
|
ggSearchBook(keyword);
|
||||||
|
cvSearchBook(keyword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user