Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0e0422010 | ||
|
|
01ab75a158 | ||
|
|
d9c10b830a | ||
|
|
35f6f4c727 | ||
|
|
3b216bfa07 | ||
|
|
e8e2f789e5 | ||
|
|
bbef41290f | ||
|
|
81b85445d8 | ||
|
|
35209ede67 | ||
|
|
0c0313f375 | ||
|
|
6bf0753978 | ||
|
|
a02f621f08 | ||
|
|
de1bc3f9af | ||
|
|
01090169a7 | ||
|
|
b564a97cdf | ||
|
|
a118fffc99 | ||
|
|
5b59aab81a | ||
|
|
7d3d0c661e | ||
|
|
f6f20ebc77 | ||
|
|
58cb54c76f | ||
|
|
3fc326fa48 | ||
|
|
a50aff67a2 | ||
|
|
3c1f5fd37f | ||
|
|
8ae066c387 | ||
|
|
0feb62c142 | ||
|
|
96b1e8960b | ||
|
|
df67079573 | ||
|
|
e3dbf7a88d | ||
|
|
8dd0585919 | ||
|
|
c830a5936e | ||
|
|
405f3c181f | ||
|
|
6d839d5cc7 | ||
|
|
bbadfa2251 | ||
|
|
7b8b2f93a0 | ||
|
|
c1030dfd13 | ||
|
|
a90177afa0 | ||
|
|
c095ee3c14 | ||
|
|
ae1f515446 | ||
|
|
0548fbb685 | ||
|
|
f22e4d996c | ||
|
|
3e0d8763c3 | ||
|
|
47f5e2ffb4 | ||
|
|
f39dc100b4 | ||
|
|
573c9f9fb4 | ||
|
|
785726deee | ||
|
|
4edd1914b4 | ||
|
|
222929e741 | ||
|
|
70b67077cc | ||
|
|
ec73558b03 | ||
|
|
bdedec90dd | ||
|
|
d45085215f | ||
|
|
b85627da5c | ||
|
|
592288cb22 | ||
|
|
2e3a3ee460 | ||
|
|
b7927a0df1 | ||
|
|
021298374e | ||
|
|
92f65882b2 | ||
|
|
f0399d04b7 | ||
|
|
9b57fa25de | ||
|
|
afbe77de3d | ||
|
|
d26d357151 | ||
|
|
4db9691cfc | ||
|
|
45c433caab | ||
|
|
25422b3411 | ||
|
|
a184f4e71a | ||
|
|
4569188008 | ||
|
|
9d9acb058d | ||
|
|
7d67168a4a | ||
|
|
09751d8b87 | ||
|
|
fd5ab0ef53 | ||
|
|
3bf173d958 | ||
|
|
98d630d453 | ||
|
|
eb2e816bfd | ||
|
|
bd01e840ca | ||
|
|
91a21ababe | ||
|
|
f4096b136e | ||
|
|
7598dfe952 | ||
|
|
5ed3b1cf53 | ||
|
|
7640ac1b3b | ||
|
|
66874f8163 | ||
|
|
3f91313303 | ||
|
|
8438b2a07b | ||
|
|
67e3721530 | ||
|
|
2252d661c0 | ||
|
|
87e526642c | ||
|
|
7f9da94a18 | ||
|
|
ec7c2db971 | ||
|
|
fcd2b68359 | ||
|
|
a1d372630d | ||
|
|
fc859afb92 | ||
|
|
2e0d0a2429 | ||
|
|
d084a06e63 | ||
|
|
e880238cb9 | ||
|
|
f58c5bee1c | ||
|
|
cbb9edac19 | ||
|
|
1b8bd27b3c | ||
|
|
6cb713d62c | ||
|
|
642af2f973 | ||
|
|
9273843062 | ||
|
|
2989586c6d | ||
|
|
5ede079401 | ||
|
|
7ad419dc8c | ||
|
|
bcdc976414 | ||
|
|
6aad9378b8 | ||
|
|
6f5390ead5 | ||
|
|
d624b67e93 | ||
|
|
01cc97c1b2 | ||
|
|
7c016265d2 | ||
|
|
974549b1af | ||
|
|
add502d236 | ||
|
|
294c594cbb | ||
|
|
60aa016734 | ||
|
|
27e8fbd248 | ||
|
|
d856c4d78e | ||
|
|
25b09a532f | ||
|
|
c1f4ca36b6 | ||
|
|
58379159fb | ||
|
|
17470b3b56 | ||
|
|
42cc13d1e2 | ||
|
|
ecc5cb167e | ||
|
|
d72210c6ae | ||
|
|
61deda1076 | ||
|
|
1e0ff0f9c2 | ||
|
|
00bf1f5ec9 | ||
|
|
2b6f5b1565 | ||
|
|
e5abc5f281 | ||
|
|
95371d0d7f | ||
|
|
c7df8a1a34 | ||
|
|
b414d91964 | ||
|
|
82164fc8e7 | ||
|
|
a5415e00d5 | ||
|
|
434029270b | ||
|
|
b3b85bf692 | ||
|
|
a36c6da3ae | ||
|
|
9e72c3b40d | ||
|
|
a0cebfbfb1 | ||
|
|
64e833f5d6 | ||
|
|
4da64ceb23 | ||
|
|
99ec99b539 | ||
|
|
3077b854d7 | ||
|
|
a8317d900b | ||
|
|
668bf3a15e | ||
|
|
481e52b503 | ||
|
|
bbb65ec804 | ||
|
|
4b7b646692 | ||
|
|
65cfb1ccbc | ||
|
|
9e9d7b3642 | ||
|
|
1294672809 | ||
|
|
857584a929 | ||
|
|
75c68d92ec | ||
|
|
b4772d9b66 | ||
|
|
382cd9458f | ||
|
|
be7ac7e163 | ||
|
|
27f71d910b | ||
|
|
9f260128cf | ||
|
|
cdd38350fe | ||
|
|
aefed40a2f | ||
|
|
88278672d8 | ||
|
|
bd0071354c | ||
|
|
c6bf62a6eb | ||
|
|
95544ef885 | ||
|
|
fe4db16a7e | ||
|
|
9b127a9f97 | ||
|
|
9e4aeac16d | ||
|
|
9653308300 | ||
|
|
7671a1d5c8 | ||
|
|
bd6b5ac873 | ||
|
|
00dc60da79 | ||
|
|
708861bcd5 | ||
|
|
24a2c0a5cf | ||
|
|
1e1d3a7c81 | ||
|
|
2ebddcfee3 | ||
|
|
4517f5b0cb | ||
|
|
666015e867 | ||
|
|
9d5e9b28ae | ||
|
|
6f1e78b9a3 | ||
|
|
d5aa345da6 | ||
|
|
e060c62742 | ||
|
|
5695268d1b | ||
|
|
cea10d3945 | ||
|
|
fe21d15194 | ||
|
|
369d5059c3 | ||
|
|
56a9c62421 | ||
|
|
50d703e2d8 | ||
|
|
5aefc893de | ||
|
|
6e5d9d7657 | ||
|
|
1be7dfcdca | ||
|
|
e58eb8dac1 | ||
|
|
d02eb842c7 | ||
|
|
f3efef1f60 | ||
|
|
659481c83b | ||
|
|
bad4c01474 | ||
|
|
cd53d57516 | ||
|
|
ac54899415 | ||
|
|
abe46e1862 | ||
|
|
3be47d6e57 | ||
|
|
25f608d109 | ||
|
|
aca5324914 | ||
|
|
d8f9e2feb2 | ||
|
|
ed26d34961 | ||
|
|
50919d4721 | ||
|
|
5edde53fed | ||
|
|
1c15e10ac0 | ||
|
|
52be2ad4a2 | ||
|
|
43fdef5e53 | ||
|
|
b699796236 | ||
|
|
b82d03c12c | ||
|
|
b8eb557761 | ||
|
|
ba40c6693e | ||
|
|
e064a3ec2b | ||
|
|
f6561456f7 | ||
|
|
deb91996a8 | ||
|
|
cd1fe6dde0 | ||
|
|
3f6a466ca7 | ||
|
|
50f4fe6546 | ||
|
|
861277460d | ||
|
|
83babf88fc | ||
|
|
58735caff3 | ||
|
|
844eb6c379 | ||
|
|
1b8410e786 | ||
|
|
fed9eff7b8 | ||
|
|
bf5de95fdc | ||
|
|
2e25e797dd | ||
|
|
c1ac68e2ae | ||
|
|
623a92ebec | ||
|
|
2edcd16119 | ||
|
|
af202bd6d1 | ||
|
|
d5a332a84e | ||
|
|
d6884164a5 | ||
|
|
707556a36d | ||
|
|
356a4f588e | ||
|
|
241b31458d | ||
|
|
efb04ddd8f | ||
|
|
781ca7a3f3 | ||
|
|
6f6d8df431 | ||
|
|
9ebde57fc3 | ||
|
|
8dc1e9bfa4 | ||
|
|
e3db2796c9 | ||
|
|
42ef049b63 | ||
|
|
8930426f2a | ||
|
|
ceffa3a108 | ||
|
|
c0a06eec46 | ||
|
|
86ef1d47e8 | ||
|
|
b835aa660a | ||
|
|
4d92d7da3a | ||
|
|
d1e6a85803 | ||
|
|
d8bad7394a | ||
|
|
f5062d1354 | ||
|
|
1d79d9ded2 | ||
|
|
10e212fcde | ||
|
|
1fa267ce1b | ||
|
|
abcf9a1808 | ||
|
|
d4cfad6363 | ||
|
|
3a24561ca2 | ||
|
|
5c19a8aacc | ||
|
|
3946ef8f0d | ||
|
|
6a2868c68d | ||
|
|
7ae3255ea9 | ||
|
|
91e6d94c83 | ||
|
|
e615073893 | ||
|
|
32e27712f0 | ||
|
|
ff39fac50f | ||
|
|
80d3ba42b4 | ||
|
|
d25cfb7499 | ||
|
|
24ad15be11 | ||
|
|
a35c635987 | ||
|
|
aa9fdd2ada | ||
|
|
afa585eb65 | ||
|
|
03acb77162 | ||
|
|
28ca39ca13 | ||
|
|
b05c17b8bb | ||
|
|
3839af65c4 | ||
|
|
275e073c42 | ||
|
|
c4876010e9 | ||
|
|
bce69b2dfc | ||
|
|
fb97e39d9f | ||
|
|
913690df78 | ||
|
|
e68a3f18fa | ||
|
|
1534827ad7 | ||
|
|
b36422bc05 | ||
|
|
1e2335dea0 | ||
|
|
e052590270 | ||
|
|
41ca85268f | ||
|
|
0e9709f304 | ||
|
|
ceec1051d5 | ||
|
|
81ccc11c9c | ||
|
|
702e96ddd6 | ||
|
|
53603f79bd | ||
|
|
250cafe814 | ||
|
|
16633ef1d3 | ||
|
|
f0a5225524 | ||
|
|
a32b36bf81 | ||
|
|
302679719d | ||
|
|
a63baa1758 | ||
|
|
752192a057 | ||
|
|
e245a147d9 | ||
|
|
0ec2bcd897 | ||
|
|
19c75d6790 | ||
|
|
0654156dd6 | ||
|
|
e4b0434733 | ||
|
|
b32721c293 | ||
|
|
c5e39a7523 | ||
|
|
0db837c0ce | ||
|
|
53dae32897 | ||
|
|
018f3ca250 | ||
|
|
a1a8a0cf29 | ||
|
|
f9c3e751f6 | ||
|
|
c7b057ec51 | ||
|
|
85ea762054 | ||
|
|
56cd62ed90 | ||
|
|
9a8c342e61 | ||
|
|
3b81ea37f4 | ||
|
|
3c8bfc31e4 | ||
|
|
59881367fe | ||
|
|
c8ebaee0f7 | ||
|
|
d5d0ad50fa | ||
|
|
6ef792d65d | ||
|
|
20fa9f5523 | ||
|
|
616cc2018a | ||
|
|
e69b1adccd | ||
|
|
280efad939 | ||
|
|
15ec6bec95 | ||
|
|
aae81c3d24 | ||
|
|
259ac94b93 | ||
|
|
a27314464a | ||
|
|
87f07003f4 | ||
|
|
1bf065fd04 | ||
|
|
f8de7e75cc | ||
|
|
7d586b3745 | ||
|
|
a56e071a19 | ||
|
|
480aecb16c | ||
|
|
ec7803fa76 | ||
|
|
91938fd18a | ||
|
|
aa2d3d2b36 | ||
|
|
d5e9cdc5b7 | ||
|
|
305e75c0ae | ||
|
|
0d247fef6a | ||
|
|
94da61c57e | ||
|
|
430ccd9ab1 | ||
|
|
47d94d9bd6 | ||
|
|
a6d1f6039d | ||
|
|
31234a4b98 | ||
|
|
476275ea53 | ||
|
|
792d4a65bc | ||
|
|
0e2dca5f4d | ||
|
|
557296b7be | ||
|
|
2236191263 | ||
|
|
1138c86868 | ||
|
|
08500c66a8 | ||
|
|
67836006c5 | ||
|
|
fa03a9ee25 | ||
|
|
bf4564e365 | ||
|
|
6a96664381 | ||
|
|
e6e3032f02 | ||
|
|
6d424f0a30 | ||
|
|
a6f0375db3 | ||
|
|
1833e8fdb4 | ||
|
|
70151c2e11 | ||
|
|
ab69962e8a | ||
|
|
075fe994af | ||
|
|
1aca1b9fdd | ||
|
|
9fea5a55f4 | ||
|
|
1a0bf45c34 | ||
|
|
67874e07b6 | ||
|
|
ced0bcfe33 | ||
|
|
8c0aa79f78 | ||
|
|
93e8c5be32 | ||
|
|
d9f86aecd2 | ||
|
|
e9552fedef | ||
|
|
84e1c6e809 | ||
|
|
aadd6fd7e0 | ||
|
|
59987ed359 | ||
|
|
a79600c81f | ||
|
|
e8a4c3c6b9 | ||
|
|
62fce26651 | ||
|
|
25add6511f | ||
|
|
b2a28cd39a | ||
|
|
6de3aebf3a | ||
|
|
1b7e422772 | ||
|
|
b0f48b7f00 | ||
|
|
94b07d05c1 | ||
|
|
4392fec7f5 | ||
|
|
c1d0ec076b | ||
|
|
a47d6cd937 | ||
|
|
dcdb5e2a9e | ||
|
|
240325dc18 | ||
|
|
e599d2ec05 | ||
|
|
c62a417cbf | ||
|
|
98fc1ee0a7 | ||
|
|
78d0fd811b | ||
|
|
a62fca1e55 | ||
|
|
1b2124aa35 | ||
|
|
010c0bfc7d | ||
|
|
251780bed6 | ||
|
|
decc2a9c79 | ||
|
|
e15324d2cd | ||
|
|
109198a374 | ||
|
|
3e5c944365 | ||
|
|
d95838309e | ||
|
|
b3648187ff | ||
|
|
b35483edf8 | ||
|
|
f29088e854 | ||
|
|
e0fac8d2c0 | ||
|
|
0a65c05083 | ||
|
|
eb2273247f | ||
|
|
02fc698f1c | ||
|
|
8dc11e89bd | ||
|
|
12c3264000 | ||
|
|
b34672ed19 | ||
|
|
f2cd93dc54 | ||
|
|
c2c4636961 | ||
|
|
c2e530af17 | ||
|
|
577b525508 | ||
|
|
541dd6b579 | ||
|
|
e57229e670 | ||
|
|
1ef2a96465 | ||
|
|
85f1ca6101 | ||
|
|
2f3f13afb9 | ||
|
|
e15ebd5aac | ||
|
|
ff99cd2456 | ||
|
|
df91ca500f | ||
|
|
eb599373ee | ||
|
|
7b529b5f9e | ||
|
|
389f0ae452 | ||
|
|
00a24756f4 | ||
|
|
9025f312cc | ||
|
|
d5e453edd3 | ||
|
|
26cc64bdd6 | ||
|
|
24bbf226a1 | ||
|
|
6014b04b2a | ||
|
|
2b7c1345ee | ||
|
|
69b7d94774 | ||
|
|
8fe762709b | ||
|
|
a3f17deb17 |
@@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem.
|
||||
- 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/LinuxServer]:
|
||||
- Docker container: [None/LinuxServer]:
|
||||
- Special Hardware: [e.g. Rasperry Pi Zero]
|
||||
- Browser: [e.g. Chrome 83.0.4103.97, Safari 13.3.7, Firefox 68.0.1 ESR]
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ __pycache__/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
.python-version
|
||||
env/
|
||||
venv/
|
||||
eggs/
|
||||
|
||||
+2
-2
@@ -41,6 +41,6 @@ Open a new GitHub pull request with the patch. Ensure the PR description clearly
|
||||
|
||||
In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consists of more than some lines of code in a local section of Calibre-Webs code. This makes it easier to test it and check all implication before it's made public.
|
||||
|
||||
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.
|
||||
Please check if your code runs with python 3, python 2 is no longer supported. If possible and the feature is related to operating system functions, try to check it on Windows and Linux.
|
||||
Calibre-Web is automatically tested on Linux in combination with python 3.8. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests.
|
||||
A static code analysis is done by Codacy, but it's partly broken and doesn't run automatically. You could check your code with ESLint before contributing, a configuration file can be found in the projects root folder.
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
|
||||
|
||||
   [](https://discord.gg/h2VsJ2NEfB)
|
||||
[](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
|
||||
[]()
|
||||
[](https://github.com/janeczku/calibre-web/releases)
|
||||
[](https://pypi.org/project/calibreweb/)
|
||||
[](https://pypi.org/project/calibreweb/)
|
||||
[](https://discord.gg/h2VsJ2NEfB)
|
||||
|
||||
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
|
||||
|
||||
@@ -14,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
||||
- full graphical setup
|
||||
- User management with fine-grained per-user permissions
|
||||
- Admin interface
|
||||
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, turkish, ukrainian
|
||||
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, korean, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
|
||||
- OPDS feed for eBook reader apps
|
||||
- Filter and search by titles, authors, tags, series and language
|
||||
- Create a custom book collection (shelves)
|
||||
@@ -32,25 +37,35 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
||||
- "Magic Link" login to make it easy to log on eReaders
|
||||
- Login via LDAP, google/github oauth and via proxy authentication
|
||||
|
||||
## Quick start
|
||||
## Installation
|
||||
|
||||
#### Installation via pip (recommended)
|
||||
1. Install calibre web via pip with the command `pip install calibreweb` (Depending on your OS and or distro the command could also be `pip3`).
|
||||
2. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-Windows) for details
|
||||
3. Calibre-Web can be started afterwards by typing `cps` or `python3 -m cps`
|
||||
|
||||
#### Manual installation
|
||||
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment.
|
||||
2. Execute the command: `python3 cps.py` (or `nohup python3 cps.py` - recommended if you want to exit the terminal window)
|
||||
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
|
||||
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
|
||||
5. Go to Login page
|
||||
|
||||
**Default admin login:**\
|
||||
Issues with Ubuntu:
|
||||
Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
|
||||
|
||||
## Quick start
|
||||
|
||||
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
|
||||
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
|
||||
Go to Login page
|
||||
|
||||
#### Default admin login:
|
||||
*Username:* admin\
|
||||
*Password:* admin123
|
||||
|
||||
**Issues with Ubuntu:**
|
||||
Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
|
||||
|
||||
## Requirements
|
||||
|
||||
python 3.x+
|
||||
python 3.5+
|
||||
|
||||
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
|
||||
|
||||
@@ -60,14 +75,7 @@ Optionally, to enable on-the-fly conversion from one ebook format to another whe
|
||||
|
||||
## Docker Images
|
||||
|
||||
Pre-built Docker images are available in these Docker Hub repositories:
|
||||
|
||||
#### **Technosoft2000 - x64**
|
||||
+ Docker Hub - [https://hub.docker.com/r/technosoft2000/calibre-web](https://hub.docker.com/r/technosoft2000/calibre-web)
|
||||
+ Github - [https://github.com/Technosoft2000/docker-calibre-web](https://github.com/Technosoft2000/docker-calibre-web)
|
||||
|
||||
Includes the Calibre `ebook-convert` binary.
|
||||
+ The "path to convertertool" should be set to `/opt/calibre/ebook-convert`
|
||||
A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team):
|
||||
|
||||
#### **LinuxServer - x64, armhf, aarch64**
|
||||
+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to ozzie.fernandez.isaacs@googlemail.com
|
||||
|
||||
## Supported Versions
|
||||
|
||||
To receive fixes for security vulnerabilities it is required to always upgrade to the latest version of Calibre-Web. See https://github.com/janeczku/calibre-web/releases/latest for the latest release.
|
||||
|
||||
## History
|
||||
|
||||
| Fixed in | Description |CVE number |
|
||||
|---------------|--------------------------------------------------------------------------------------------------------------------|---------|
|
||||
| 3rd July 2018 | Guest access acts as a backdoor ||
|
||||
| V 0.6.7 | Hardcoded secret key for sessions |CVE-2020-12627 |
|
||||
| V 0.6.13 | Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
||||
| V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo ||
|
||||
| V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) ||
|
||||
| V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field ||
|
||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code ||
|
||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code ||
|
||||
| V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title ||
|
||||
| V 0.6.13 | JavaScript could get executed in the shelf title ||
|
||||
| V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
|
||||
| V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 ||
|
||||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 ||
|
||||
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.16 | JavaScript could get executed on authors page. Thanks to @alicaz ||
|
||||
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 ||
|
||||
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @ibarrionuevo ||
|
||||
|
||||
|
||||
## Staement regarding Log4j (CVE-2021-44228 and related)
|
||||
|
||||
Calibre-web is not affected by bugs related to Log4j. Calibre-Web is a python program, therefore not using Java, and not using the Java logging feature log4j.
|
||||
@@ -17,18 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
# Insert local directories into path
|
||||
if sys.version_info < (3, 0):
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))))
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))), 'vendor'))
|
||||
else:
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
|
||||
|
||||
|
||||
from cps import create_app
|
||||
@@ -42,13 +37,14 @@ from cps.admin import admi
|
||||
from cps.gdrive import gdrive
|
||||
from cps.editbooks import editbook
|
||||
from cps.remotelogin import remotelogin
|
||||
from cps.search_metadata import meta
|
||||
from cps.error_handler import init_errorhandler
|
||||
|
||||
try:
|
||||
from cps.kobo import kobo, get_kobo_activated
|
||||
from cps.kobo_auth import kobo_auth
|
||||
kobo_available = get_kobo_activated()
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
||||
kobo_available = False
|
||||
|
||||
try:
|
||||
@@ -70,7 +66,7 @@ def main():
|
||||
app.register_blueprint(shelf)
|
||||
app.register_blueprint(admi)
|
||||
app.register_blueprint(remotelogin)
|
||||
# if config.config_use_google_drive:
|
||||
app.register_blueprint(meta)
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(editbook)
|
||||
if kobo_available:
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
|
||||
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
|
||||
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
|
||||
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
|
||||
# apetresc, nanu-c, mutschler, GammaC0de, vuolter
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask_login import LoginManager
|
||||
from flask import session
|
||||
|
||||
|
||||
class MyLoginManager(LoginManager):
|
||||
def _session_protection_failed(self):
|
||||
sess = session._get_current_object()
|
||||
ident = self._session_identifier_generator()
|
||||
if(sess and not (len(sess) == 1 and sess.get('csrf_token', None))) and ident != sess.get('_id', None):
|
||||
return super(). _session_protection_failed()
|
||||
return False
|
||||
+52
-14
@@ -19,8 +19,8 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
__package__ = "cps"
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import mimetypes
|
||||
@@ -29,14 +29,26 @@ from babel import Locale as LC
|
||||
from babel import negotiate_locale
|
||||
from babel.core import UnknownLocaleError
|
||||
from flask import Flask, request, g
|
||||
from flask_login import LoginManager
|
||||
from .MyLoginManager import MyLoginManager
|
||||
from flask_babel import Babel
|
||||
from flask_principal import Principal
|
||||
|
||||
from . import config_sql, logger, cache_buster, cli, ub, db
|
||||
from .reverseproxy import ReverseProxied
|
||||
from .server import WebServer
|
||||
from .dep_check import dependency_check
|
||||
|
||||
try:
|
||||
import lxml
|
||||
lxml_present = True
|
||||
except ImportError:
|
||||
lxml_present = False
|
||||
|
||||
try:
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
wtf_present = True
|
||||
except ImportError:
|
||||
wtf_present = False
|
||||
|
||||
mimetypes.init()
|
||||
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
||||
@@ -56,20 +68,29 @@ mimetypes.add_type('application/mp4', '.m4a')
|
||||
mimetypes.add_type('application/mp4', '.m4b')
|
||||
mimetypes.add_type('application/ogg', '.ogg')
|
||||
mimetypes.add_type('application/ogg', '.oga')
|
||||
mimetypes.add_type('text/css', '.css')
|
||||
mimetypes.add_type('text/javascript; charset=UTF-8', '.js')
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.update(
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
SESSION_COOKIE_SAMESITE='Lax',
|
||||
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
||||
WTF_CSRF_SSL_STRICT=False
|
||||
)
|
||||
|
||||
|
||||
lm = LoginManager()
|
||||
lm = MyLoginManager()
|
||||
lm.login_view = 'web.login'
|
||||
lm.anonymous_user = ub.Anonymous
|
||||
lm.session_protection = 'strong'
|
||||
|
||||
if wtf_present:
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
else:
|
||||
csrf = None
|
||||
|
||||
ub.init_db(cli.settingspath)
|
||||
# pylint: disable=no-member
|
||||
config = config_sql.load_configuration(ub.session)
|
||||
@@ -81,27 +102,44 @@ _BABEL_TRANSLATIONS = set()
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
from . import services
|
||||
|
||||
db.CalibreDB.setup_db(config, cli.settingspath)
|
||||
db.CalibreDB.update_config(config)
|
||||
db.CalibreDB.setup_db(config.config_calibre_dir, cli.settingspath)
|
||||
|
||||
|
||||
calibre_db = db.CalibreDB()
|
||||
|
||||
def create_app():
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
# For python2 convert path to unicode
|
||||
if sys.version_info < (3, 0):
|
||||
app.static_folder = app.static_folder.decode('utf-8')
|
||||
app.root_path = app.root_path.decode('utf-8')
|
||||
app.instance_path = app.instance_path.decode('utf-8')
|
||||
log.info(
|
||||
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
||||
print(
|
||||
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
||||
web_server.stop(True)
|
||||
sys.exit(5)
|
||||
if not lxml_present:
|
||||
log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||
print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||
web_server.stop(True)
|
||||
sys.exit(6)
|
||||
if not wtf_present:
|
||||
log.info('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
|
||||
print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
|
||||
web_server.stop(True)
|
||||
sys.exit(7)
|
||||
for res in dependency_check() + dependency_check(True):
|
||||
log.info('*** "{}" version does not fit the requirements. Should: {}, Found: {}, please consider installing required version ***'
|
||||
.format(res['name'],
|
||||
res['target'],
|
||||
res['found']))
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
|
||||
if os.environ.get('FLASK_DEBUG'):
|
||||
cache_buster.init_cache_busting(app)
|
||||
|
||||
log.info('Starting Calibre Web...')
|
||||
if sys.version_info < (3, 0):
|
||||
log.info('Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2 please consider upgrading to Python3')
|
||||
print('Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2 please consider upgrading to Python3')
|
||||
|
||||
Principal(app)
|
||||
lm.init_app(app)
|
||||
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
||||
@@ -125,7 +163,6 @@ def create_app():
|
||||
def get_locale():
|
||||
# if a user is logged in, use the locale from the user settings
|
||||
user = getattr(g, 'user', None)
|
||||
# user = None
|
||||
if user is not None and hasattr(user, "locale"):
|
||||
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
||||
return user.locale
|
||||
@@ -146,6 +183,7 @@ def get_timezone():
|
||||
user = getattr(g, 'user', None)
|
||||
return user.timezone if user else None
|
||||
|
||||
|
||||
from .updater import Updater
|
||||
updater_thread = Updater()
|
||||
updater_thread.start()
|
||||
|
||||
+69
-32
@@ -20,7 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import platform
|
||||
import sqlite3
|
||||
@@ -29,9 +28,14 @@ from collections import OrderedDict
|
||||
import babel, pytz, requests, sqlalchemy
|
||||
import werkzeug, flask, flask_login, flask_principal, jinja2
|
||||
from flask_babel import gettext as _
|
||||
try:
|
||||
from flask_wtf import __version__ as flaskwtf_version
|
||||
except ImportError:
|
||||
flaskwtf_version = _(u'not installed')
|
||||
|
||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants, gdriveutils, dep_check
|
||||
from .render_template import render_title_template
|
||||
|
||||
try:
|
||||
from flask_login import __version__ as flask_loginVersion
|
||||
except ImportError:
|
||||
@@ -54,40 +58,75 @@ try:
|
||||
except ImportError:
|
||||
greenlet_Version = None
|
||||
|
||||
try:
|
||||
from scholarly import scholarly
|
||||
scholarly_version = _(u'installed')
|
||||
except ImportError:
|
||||
scholarly_version = _(u'not installed')
|
||||
|
||||
from . import services
|
||||
|
||||
about = flask.Blueprint('about', __name__)
|
||||
|
||||
ret = dict()
|
||||
req = dep_check.load_dependencys(False)
|
||||
opt = dep_check.load_dependencys(True)
|
||||
for i in (req + opt):
|
||||
ret[i[1]] = i[0]
|
||||
|
||||
_VERSIONS = OrderedDict(
|
||||
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||
Python=sys.version,
|
||||
Calibre_Web=constants.STABLE_VERSION['version'] + ' - '
|
||||
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
|
||||
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
|
||||
WebServer=server.VERSION,
|
||||
Flask=flask.__version__,
|
||||
Flask_Login=flask_loginVersion,
|
||||
Flask_Principal=flask_principal.__version__,
|
||||
Werkzeug=werkzeug.__version__,
|
||||
Babel=babel.__version__,
|
||||
Jinja2=jinja2.__version__,
|
||||
Requests=requests.__version__,
|
||||
SqlAlchemy=sqlalchemy.__version__,
|
||||
pySqlite=sqlite3.version,
|
||||
SQLite=sqlite3.sqlite_version,
|
||||
iso639=isoLanguages.__version__,
|
||||
pytz=pytz.__version__,
|
||||
Unidecode = unidecode_version,
|
||||
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None,
|
||||
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,
|
||||
greenlet = greenlet_Version
|
||||
)
|
||||
_VERSIONS.update(uploader.get_versions())
|
||||
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
|
||||
calibre_web_version = constants.STABLE_VERSION['version']
|
||||
else:
|
||||
calibre_web_version = (constants.STABLE_VERSION['version'] + ' - '
|
||||
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
|
||||
+ constants.NIGHTLY_VERSION[1].replace('%','%%'))
|
||||
if getattr(sys, 'frozen', False):
|
||||
calibre_web_version += " - Exe-Version"
|
||||
elif constants.HOME_CONFIG:
|
||||
calibre_web_version += " - pyPi"
|
||||
|
||||
if not ret:
|
||||
_VERSIONS = OrderedDict(
|
||||
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||
Python=sys.version,
|
||||
Calibre_Web=calibre_web_version,
|
||||
WebServer=server.VERSION,
|
||||
Flask=flask.__version__,
|
||||
Flask_Login=flask_loginVersion,
|
||||
Flask_Principal=flask_principal.__version__,
|
||||
Flask_WTF=flaskwtf_version,
|
||||
Werkzeug=werkzeug.__version__,
|
||||
Babel=babel.__version__,
|
||||
Jinja2=jinja2.__version__,
|
||||
Requests=requests.__version__,
|
||||
SqlAlchemy=sqlalchemy.__version__,
|
||||
pySqlite=sqlite3.version,
|
||||
SQLite=sqlite3.sqlite_version,
|
||||
iso639=isoLanguages.__version__,
|
||||
pytz=pytz.__version__,
|
||||
Unidecode=unidecode_version,
|
||||
Scholarly=scholarly_version,
|
||||
Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
|
||||
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,
|
||||
greenlet=greenlet_Version
|
||||
)
|
||||
_VERSIONS.update(gdriveutils.get_versions())
|
||||
_VERSIONS.update(uploader.get_versions(True))
|
||||
else:
|
||||
_VERSIONS = OrderedDict(
|
||||
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||
Python = sys.version,
|
||||
Calibre_Web=calibre_web_version,
|
||||
Werkzeug = werkzeug.__version__,
|
||||
Jinja2=jinja2.__version__,
|
||||
pySqlite = sqlite3.version,
|
||||
SQLite = sqlite3.sqlite_version,
|
||||
)
|
||||
_VERSIONS.update(ret)
|
||||
_VERSIONS.update(uploader.get_versions(False))
|
||||
|
||||
def collect_stats():
|
||||
_VERSIONS['ebook converter'] = _(converter.get_calibre_version())
|
||||
@@ -104,5 +143,3 @@ def stats():
|
||||
series = calibre_db.session.query(db.Series).count()
|
||||
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
|
||||
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
|
||||
|
||||
|
||||
|
||||
+270
-160
@@ -20,7 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import re
|
||||
import base64
|
||||
@@ -34,14 +33,14 @@ from babel.dates import format_datetime
|
||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
||||
from flask_login import login_required, current_user, logout_user, confirm_login
|
||||
from flask_babel import gettext as _
|
||||
from flask import session as flask_session
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
||||
from sqlalchemy.sql.expression import func, or_, text
|
||||
|
||||
from . import constants, logger, helper, services
|
||||
from .cli import filepicker
|
||||
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
|
||||
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils, kobo_sync_status
|
||||
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
|
||||
valid_email, check_username
|
||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||
@@ -97,23 +96,13 @@ def admin_required(f):
|
||||
return inner
|
||||
|
||||
|
||||
def unconfigured(f):
|
||||
"""
|
||||
Checks if calibre-web instance is not configured
|
||||
"""
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
if not config.db_configured:
|
||||
return f(*args, **kwargs)
|
||||
abort(403)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@admi.before_app_request
|
||||
def before_request():
|
||||
# make remember me function work
|
||||
if current_user.is_authenticated:
|
||||
confirm_login()
|
||||
if not ub.check_user_session(current_user.id, flask_session.get('_id')) and 'opds' not in request.path:
|
||||
logout_user()
|
||||
g.constants = constants
|
||||
g.user = current_user
|
||||
g.allow_registration = config.config_public_reg
|
||||
@@ -124,10 +113,14 @@ def before_request():
|
||||
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
||||
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
||||
if '/static/' not in request.path and not config.db_configured and \
|
||||
request.endpoint not in ('admin.basic_configuration',
|
||||
'login',
|
||||
'admin.config_pathchooser'):
|
||||
return redirect(url_for('admin.basic_configuration'))
|
||||
request.endpoint not in ('admin.ajax_db_config',
|
||||
'admin.simulatedbchange',
|
||||
'admin.db_configuration',
|
||||
'web.login',
|
||||
'web.logout',
|
||||
'admin.load_dialogtexts',
|
||||
'admin.ajax_pathchooser'):
|
||||
return redirect(url_for('admin.db_configuration'))
|
||||
|
||||
|
||||
@admi.route("/admin")
|
||||
@@ -136,11 +129,11 @@ def admin_forbidden():
|
||||
abort(403)
|
||||
|
||||
|
||||
@admi.route("/shutdown")
|
||||
@admi.route("/shutdown", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def shutdown():
|
||||
task = int(request.args.get("parameter").strip())
|
||||
task = request.get_json().get('parameter', -1)
|
||||
showtext = {}
|
||||
if task in (0, 1): # valid commandos received
|
||||
# close all database connections
|
||||
@@ -152,7 +145,7 @@ def shutdown():
|
||||
else:
|
||||
showtext['text'] = _(u'Performing shutdown of server, please close window')
|
||||
# stop gevent/tornado server
|
||||
web_server.stop(task==0)
|
||||
web_server.stop(task == 0)
|
||||
return json.dumps(showtext)
|
||||
|
||||
if task == 2:
|
||||
@@ -194,16 +187,46 @@ def admin():
|
||||
feature_support=feature_support, kobo_support=kobo_support,
|
||||
title=_(u"Admin page"), page="admin")
|
||||
|
||||
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def db_configuration():
|
||||
if request.method == "POST":
|
||||
return _db_configuration_update_helper()
|
||||
return _db_configuration_result()
|
||||
|
||||
@admi.route("/admin/config", methods=["GET", "POST"])
|
||||
|
||||
@admi.route("/admin/config", methods=["GET"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def configuration():
|
||||
if request.method == "POST":
|
||||
return _configuration_update_helper(True)
|
||||
return _configuration_result()
|
||||
return render_title_template("config_edit.html",
|
||||
config=config,
|
||||
provider=oauthblueprints,
|
||||
feature_support=feature_support,
|
||||
title=_(u"Basic Configuration"), page="config")
|
||||
|
||||
|
||||
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def ajax_config():
|
||||
return _configuration_update_helper()
|
||||
|
||||
|
||||
@admi.route("/admin/ajaxdbconfig", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def ajax_db_config():
|
||||
return _db_configuration_update_helper()
|
||||
|
||||
|
||||
@admi.route("/admin/alive", methods=["GET"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def calibreweb_alive():
|
||||
return "", 200
|
||||
|
||||
@admi.route("/admin/viewconfig")
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -212,8 +235,12 @@ def view_configuration():
|
||||
.filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all()
|
||||
restrict_columns = calibre_db.session.query(db.Custom_Columns)\
|
||||
.filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all()
|
||||
languages = calibre_db.speaking_language()
|
||||
translations = [LC('en')] + babel.list_translations()
|
||||
return render_title_template("config_view_edit.html", conf=config, readColumns=read_column,
|
||||
restrictColumns=restrict_columns,
|
||||
languages=languages,
|
||||
translations=translations,
|
||||
title=_(u"UI Configuration"), page="uiconfig")
|
||||
|
||||
@admi.route("/admin/usertable")
|
||||
@@ -236,7 +263,7 @@ def edit_user_table():
|
||||
custom_values = []
|
||||
if not config.config_anonbrowse:
|
||||
allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
|
||||
|
||||
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||
return render_title_template("user_table.html",
|
||||
users=allUser.all(),
|
||||
tags=tags,
|
||||
@@ -245,6 +272,7 @@ def edit_user_table():
|
||||
languages=languages,
|
||||
visiblility=visibility,
|
||||
all_roles=constants.ALL_ROLES,
|
||||
kobo_support=kobo_support,
|
||||
sidebar_settings=constants.sidebar_settings,
|
||||
title=_(u"Edit Users"),
|
||||
page="usertable")
|
||||
@@ -391,6 +419,8 @@ def edit_list_user(param):
|
||||
user.name = check_username(vals['value'])
|
||||
elif param =='email':
|
||||
user.email = check_email(vals['value'])
|
||||
elif param =='kobo_only_shelves_sync':
|
||||
user.kobo_only_shelves_sync = int(vals['value'] == 'true')
|
||||
elif param == 'kindle_mail':
|
||||
user.kindle_mail = valid_email(vals['value']) if vals['value'] else ""
|
||||
elif param.endswith('role'):
|
||||
@@ -488,37 +518,35 @@ def check_valid_restricted_column(column):
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@admi.route("/admin/viewconfig", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def update_view_configuration():
|
||||
to_save = request.form.to_dict()
|
||||
|
||||
_config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
|
||||
_config_int = lambda x: config.set_from_dictionary(to_save, x, int)
|
||||
|
||||
_config_string("config_calibre_web_title")
|
||||
_config_string("config_columns_to_ignore")
|
||||
if _config_string("config_title_regex"):
|
||||
_config_string(to_save, "config_calibre_web_title")
|
||||
_config_string(to_save, "config_columns_to_ignore")
|
||||
if _config_string(to_save, "config_title_regex"):
|
||||
calibre_db.update_title_sort(config)
|
||||
|
||||
if not check_valid_read_column(to_save.get("config_read_column", "0")):
|
||||
flash(_(u"Invalid Read Column"), category="error")
|
||||
log.debug("Invalid Read column")
|
||||
return view_configuration()
|
||||
_config_int("config_read_column")
|
||||
_config_int(to_save, "config_read_column")
|
||||
|
||||
if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")):
|
||||
flash(_(u"Invalid Restricted Column"), category="error")
|
||||
log.debug("Invalid Restricted Column")
|
||||
return view_configuration()
|
||||
_config_int("config_restricted_column")
|
||||
_config_int(to_save, "config_restricted_column")
|
||||
|
||||
_config_int("config_theme")
|
||||
_config_int("config_random_books")
|
||||
_config_int("config_books_per_page")
|
||||
_config_int("config_authors_max")
|
||||
_config_int(to_save, "config_theme")
|
||||
_config_int(to_save, "config_random_books")
|
||||
_config_int(to_save, "config_books_per_page")
|
||||
_config_int(to_save, "config_authors_max")
|
||||
_config_string(to_save, "config_default_language")
|
||||
_config_string(to_save, "config_default_locale")
|
||||
|
||||
|
||||
config.config_default_role = constants.selected_roles(to_save)
|
||||
@@ -536,10 +564,10 @@ def update_view_configuration():
|
||||
return view_configuration()
|
||||
|
||||
|
||||
@admi.route("/ajax/loaddialogtexts/<element_id>")
|
||||
@admi.route("/ajax/loaddialogtexts/<element_id>", methods=['POST'])
|
||||
@login_required
|
||||
def load_dialogtexts(element_id):
|
||||
texts = {"header": "", "main": ""}
|
||||
texts = {"header": "", "main": "", "valid": 1}
|
||||
if element_id == "config_delete_kobo_token":
|
||||
texts["main"] = _('Do you really want to delete the Kobo Token?')
|
||||
elif element_id == "btndeletedomain":
|
||||
@@ -558,6 +586,12 @@ def load_dialogtexts(element_id):
|
||||
texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?')
|
||||
elif element_id == "sidebar_view":
|
||||
texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?')
|
||||
elif element_id == "kobo_only_shelves_sync":
|
||||
texts["main"] = _('Are you sure you want to change shelf sync behavior for the selected user(s)?')
|
||||
elif element_id == "db_submit":
|
||||
texts["main"] = _('Are you sure you want to change Calibre library location?')
|
||||
elif element_id == "btnfullsync":
|
||||
texts["main"] = _("Are you sure you want delete Calibre-Web's sync database to force a full sync with your Kobo Reader?")
|
||||
return json.dumps(texts)
|
||||
|
||||
|
||||
@@ -722,7 +756,12 @@ def prepare_tags(user, action, tags_name, id_list):
|
||||
return ",".join(saved_tags_list)
|
||||
|
||||
|
||||
@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
||||
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def add_user_0_restriction(res_type):
|
||||
return add_restriction(res_type, 0)
|
||||
|
||||
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -769,7 +808,13 @@ def add_restriction(res_type, user_id):
|
||||
return ""
|
||||
|
||||
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_user_0_restriction(res_type):
|
||||
return delete_restriction(res_type, 0)
|
||||
|
||||
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -857,17 +902,17 @@ def list_restriction(res_type, user_id):
|
||||
else:
|
||||
json_dumps = ""
|
||||
js = json.dumps(json_dumps)
|
||||
response = make_response(js.replace("'", '"'))
|
||||
response = make_response(js)
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
|
||||
@admi.route("/basicconfig/pathchooser/")
|
||||
@unconfigured
|
||||
def config_pathchooser():
|
||||
if filepicker:
|
||||
return pathchooser()
|
||||
abort(403)
|
||||
@admi.route("/ajax/fullsync", methods=["POST"])
|
||||
@login_required
|
||||
def ajax_fullsync():
|
||||
count = ub.session.query(ub.KoboSyncedBooks).filter(current_user.id == ub.KoboSyncedBooks.user_id).delete()
|
||||
message = _("{} sync entries deleted").format(count)
|
||||
ub.session_commit(message)
|
||||
return Response(json.dumps([{"type": "success", "message": message}]), mimetype='application/json')
|
||||
|
||||
|
||||
@admi.route("/ajax/pathchooser/")
|
||||
@@ -958,16 +1003,6 @@ def pathchooser():
|
||||
return json.dumps(context)
|
||||
|
||||
|
||||
@admi.route("/basicconfig", methods=["GET", "POST"])
|
||||
@unconfigured
|
||||
def basic_configuration():
|
||||
logout_user()
|
||||
if request.method == "POST":
|
||||
log.debug("Basic Configuration send")
|
||||
return _configuration_update_helper(configured=filepicker)
|
||||
return _configuration_result(configured=filepicker)
|
||||
|
||||
|
||||
def _config_int(to_save, x, func=int):
|
||||
return config.set_from_dictionary(to_save, x, func)
|
||||
|
||||
@@ -986,23 +1021,24 @@ def _config_string(to_save, x):
|
||||
|
||||
def _configuration_gdrive_helper(to_save):
|
||||
gdrive_error = None
|
||||
gdrive_secrets = {}
|
||||
if to_save.get("config_use_google_drive"):
|
||||
gdrive_secrets = {}
|
||||
|
||||
if not os.path.isfile(gdriveutils.SETTINGS_YAML):
|
||||
config.config_use_google_drive = False
|
||||
if not os.path.isfile(gdriveutils.SETTINGS_YAML):
|
||||
config.config_use_google_drive = False
|
||||
|
||||
if gdrive_support:
|
||||
gdrive_error = gdriveutils.get_error_text(gdrive_secrets)
|
||||
if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error:
|
||||
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
||||
gdrive_secrets = json.load(settings)['web']
|
||||
if not gdrive_secrets:
|
||||
return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
|
||||
gdriveutils.update_settings(
|
||||
gdrive_secrets['client_id'],
|
||||
gdrive_secrets['client_secret'],
|
||||
gdrive_secrets['redirect_uris'][0]
|
||||
)
|
||||
if gdrive_support:
|
||||
gdrive_error = gdriveutils.get_error_text(gdrive_secrets)
|
||||
if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error:
|
||||
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
||||
gdrive_secrets = json.load(settings)['web']
|
||||
if not gdrive_secrets:
|
||||
return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
|
||||
gdriveutils.update_settings(
|
||||
gdrive_secrets['client_id'],
|
||||
gdrive_secrets['client_secret'],
|
||||
gdrive_secrets['redirect_uris'][0]
|
||||
)
|
||||
|
||||
# always show google drive settings, but in case of error deny support
|
||||
new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save)
|
||||
@@ -1036,23 +1072,23 @@ def _configuration_oauth_helper(to_save):
|
||||
return reboot_required
|
||||
|
||||
|
||||
def _configuration_logfile_helper(to_save, gdrive_error):
|
||||
def _configuration_logfile_helper(to_save):
|
||||
reboot_required = False
|
||||
reboot_required |= _config_int(to_save, "config_log_level")
|
||||
reboot_required |= _config_string(to_save, "config_logfile")
|
||||
if not logger.is_valid_logfile(config.config_logfile):
|
||||
return reboot_required, \
|
||||
_configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error)
|
||||
_configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'))
|
||||
|
||||
reboot_required |= _config_checkbox_int(to_save, "config_access_log")
|
||||
reboot_required |= _config_string(to_save, "config_access_logfile")
|
||||
if not logger.is_valid_logfile(config.config_access_logfile):
|
||||
return reboot_required, \
|
||||
_configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error)
|
||||
_configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'))
|
||||
return reboot_required, None
|
||||
|
||||
|
||||
def _configuration_ldap_helper(to_save, gdrive_error):
|
||||
def _configuration_ldap_helper(to_save):
|
||||
reboot_required = False
|
||||
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
|
||||
reboot_required |= _config_int(to_save, "config_ldap_port")
|
||||
@@ -1079,44 +1115,37 @@ def _configuration_ldap_helper(to_save, gdrive_error):
|
||||
or not config.config_ldap_dn \
|
||||
or not config.config_ldap_user_object:
|
||||
return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, '
|
||||
'Port, DN and User Object Identifier'), gdrive_error)
|
||||
'Port, DN and User Object Identifier'))
|
||||
|
||||
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
|
||||
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
|
||||
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password):
|
||||
return reboot_required, _configuration_result('Please Enter a LDAP Service Account and Password',
|
||||
gdrive_error)
|
||||
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password'))
|
||||
else:
|
||||
if not config.config_ldap_serv_username:
|
||||
return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdrive_error)
|
||||
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account'))
|
||||
|
||||
if config.config_ldap_group_object_filter:
|
||||
if config.config_ldap_group_object_filter.count("%s") != 1:
|
||||
return reboot_required, \
|
||||
_configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'),
|
||||
gdrive_error)
|
||||
_configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'))
|
||||
if config.config_ldap_group_object_filter.count("(") != config.config_ldap_group_object_filter.count(")"):
|
||||
return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'),
|
||||
gdrive_error)
|
||||
return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'))
|
||||
|
||||
if config.config_ldap_user_object.count("%s") != 1:
|
||||
return reboot_required, \
|
||||
_configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'),
|
||||
gdrive_error)
|
||||
_configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'))
|
||||
if config.config_ldap_user_object.count("(") != config.config_ldap_user_object.count(")"):
|
||||
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'),
|
||||
gdrive_error)
|
||||
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'))
|
||||
|
||||
if to_save.get("ldap_import_user_filter") == '0':
|
||||
config.config_ldap_member_user_object = ""
|
||||
else:
|
||||
if config.config_ldap_member_user_object.count("%s") != 1:
|
||||
return reboot_required, \
|
||||
_configuration_result(_('LDAP Member User Filter needs to Have One "%s" Format Identifier'),
|
||||
gdrive_error)
|
||||
_configuration_result(_('LDAP Member User Filter needs to Have One "%s" Format Identifier'))
|
||||
if config.config_ldap_member_user_object.count("(") != config.config_ldap_member_user_object.count(")"):
|
||||
return reboot_required, _configuration_result(_('LDAP Member User Filter Has Unmatched Parenthesis'),
|
||||
gdrive_error)
|
||||
return reboot_required, _configuration_result(_('LDAP Member User Filter Has Unmatched Parenthesis'))
|
||||
|
||||
if config.config_ldap_cacert_path or config.config_ldap_cert_path or config.config_ldap_key_path:
|
||||
if not (os.path.isfile(config.config_ldap_cacert_path) and
|
||||
@@ -1124,13 +1153,31 @@ def _configuration_ldap_helper(to_save, gdrive_error):
|
||||
os.path.isfile(config.config_ldap_key_path)):
|
||||
return reboot_required, \
|
||||
_configuration_result(_('LDAP CACertificate, Certificate or Key Location is not Valid, '
|
||||
'Please Enter Correct Path'),
|
||||
gdrive_error)
|
||||
'Please Enter Correct Path'))
|
||||
return reboot_required, None
|
||||
|
||||
|
||||
def _configuration_update_helper(configured):
|
||||
reboot_required = False
|
||||
@admi.route("/ajax/simulatedbchange", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def simulatedbchange():
|
||||
db_change, db_valid = _db_simulate_change()
|
||||
return Response(json.dumps({"change": db_change, "valid": db_valid}), mimetype='application/json')
|
||||
|
||||
|
||||
def _db_simulate_change():
|
||||
param = request.form.to_dict()
|
||||
to_save = {}
|
||||
to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$',
|
||||
'',
|
||||
param['config_calibre_dir'],
|
||||
flags=re.IGNORECASE).strip()
|
||||
db_change = config.config_calibre_dir != to_save["config_calibre_dir"] and config.config_calibre_dir
|
||||
db_valid = calibre_db.check_valid_db(to_save["config_calibre_dir"], ub.app_DB_path)
|
||||
return db_change, db_valid
|
||||
|
||||
|
||||
def _db_configuration_update_helper():
|
||||
db_change = False
|
||||
to_save = request.form.to_dict()
|
||||
gdrive_error = None
|
||||
@@ -1140,26 +1187,59 @@ def _configuration_update_helper(configured):
|
||||
to_save['config_calibre_dir'],
|
||||
flags=re.IGNORECASE)
|
||||
try:
|
||||
db_change |= _config_string(to_save, "config_calibre_dir")
|
||||
db_change, db_valid = _db_simulate_change()
|
||||
|
||||
# gdrive_error drive setup
|
||||
gdrive_error = _configuration_gdrive_helper(to_save)
|
||||
except (OperationalError, InvalidRequestError):
|
||||
ub.session.rollback()
|
||||
log.error("Settings DB is not Writeable")
|
||||
_db_configuration_result(_("Settings DB is not Writeable"), gdrive_error)
|
||||
try:
|
||||
metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db")
|
||||
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
|
||||
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
|
||||
db_change = True
|
||||
except Exception as ex:
|
||||
return _db_configuration_result('{}'.format(ex), gdrive_error)
|
||||
|
||||
if db_change or not db_valid or not config.db_configured:
|
||||
if not calibre_db.setup_db(to_save['config_calibre_dir'], ub.app_DB_path):
|
||||
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
|
||||
gdrive_error)
|
||||
# if db changed -> delete shelfs, delete download books, delete read books, kobo sync...
|
||||
ub.session.query(ub.Downloads).delete()
|
||||
ub.session.query(ub.ArchivedBook).delete()
|
||||
ub.session.query(ub.ReadBook).delete()
|
||||
ub.session.query(ub.BookShelf).delete()
|
||||
ub.session.query(ub.Bookmark).delete()
|
||||
ub.session.query(ub.KoboReadingState).delete()
|
||||
ub.session.query(ub.KoboStatistics).delete()
|
||||
ub.session.query(ub.KoboSyncedBooks).delete()
|
||||
ub.session_commit()
|
||||
_config_string(to_save, "config_calibre_dir")
|
||||
calibre_db.update_config(config)
|
||||
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
||||
flash(_(u"DB is not Writeable"), category="warning")
|
||||
config.save()
|
||||
return _db_configuration_result(None, gdrive_error)
|
||||
|
||||
def _configuration_update_helper():
|
||||
reboot_required = False
|
||||
to_save = request.form.to_dict()
|
||||
try:
|
||||
reboot_required |= _config_int(to_save, "config_port")
|
||||
|
||||
reboot_required |= _config_string(to_save, "config_trustedhosts")
|
||||
reboot_required |= _config_string(to_save, "config_keyfile")
|
||||
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
|
||||
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'),
|
||||
gdrive_error,
|
||||
configured)
|
||||
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'))
|
||||
|
||||
reboot_required |= _config_string(to_save, "config_certfile")
|
||||
if config.config_certfile and not os.path.isfile(config.config_certfile):
|
||||
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'),
|
||||
gdrive_error,
|
||||
configured)
|
||||
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'))
|
||||
|
||||
_config_checkbox_int(to_save, "config_uploading")
|
||||
_config_checkbox_int(to_save, "config_unicode_filename")
|
||||
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
||||
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
|
||||
and config.config_login_type == constants.LOGIN_LDAP)
|
||||
@@ -1181,15 +1261,14 @@ def _configuration_update_helper(configured):
|
||||
|
||||
reboot_required |= _config_int(to_save, "config_login_type")
|
||||
|
||||
# LDAP configurator,
|
||||
# LDAP configurator
|
||||
if config.config_login_type == constants.LOGIN_LDAP:
|
||||
reboot, message = _configuration_ldap_helper(to_save, gdrive_error)
|
||||
reboot, message = _configuration_ldap_helper(to_save)
|
||||
if message:
|
||||
return message
|
||||
reboot_required |= reboot
|
||||
|
||||
# Remote login configuration
|
||||
|
||||
_config_checkbox(to_save, "config_remote_login")
|
||||
if not config.config_remote_login:
|
||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type == 0).delete()
|
||||
@@ -1213,7 +1292,7 @@ def _configuration_update_helper(configured):
|
||||
if config.config_login_type == constants.LOGIN_OAUTH:
|
||||
reboot_required |= _configuration_oauth_helper(to_save)
|
||||
|
||||
reboot, message = _configuration_logfile_helper(to_save, gdrive_error)
|
||||
reboot, message = _configuration_logfile_helper(to_save)
|
||||
if message:
|
||||
return message
|
||||
reboot_required |= reboot
|
||||
@@ -1222,67 +1301,57 @@ def _configuration_update_helper(configured):
|
||||
if "config_rarfile_location" in to_save:
|
||||
unrar_status = helper.check_unrar(config.config_rarfile_location)
|
||||
if unrar_status:
|
||||
return _configuration_result(unrar_status, gdrive_error, configured)
|
||||
return _configuration_result(unrar_status)
|
||||
except (OperationalError, InvalidRequestError):
|
||||
ub.session.rollback()
|
||||
log.error("Settings DB is not Writeable")
|
||||
_configuration_result(_("Settings DB is not Writeable"), gdrive_error, configured)
|
||||
|
||||
try:
|
||||
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
|
||||
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
|
||||
db_change = True
|
||||
except Exception as ex:
|
||||
return _configuration_result('%s' % ex, gdrive_error, configured)
|
||||
|
||||
if db_change:
|
||||
if not calibre_db.setup_db(config, ub.app_DB_path):
|
||||
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
|
||||
gdrive_error,
|
||||
configured)
|
||||
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
||||
flash(_(u"DB is not Writeable"), category="warning")
|
||||
_configuration_result(_("Settings DB is not Writeable"))
|
||||
|
||||
config.save()
|
||||
flash(_(u"Calibre-Web configuration updated"), category="success")
|
||||
if reboot_required:
|
||||
web_server.stop(True)
|
||||
|
||||
return _configuration_result(None, gdrive_error, configured)
|
||||
return _configuration_result(None, reboot_required)
|
||||
|
||||
def _configuration_result(error_flash=None, reboot=False):
|
||||
resp = {}
|
||||
if error_flash:
|
||||
log.error(error_flash)
|
||||
config.load()
|
||||
resp['result'] = [{'type': "danger", 'message': error_flash}]
|
||||
else:
|
||||
resp['result'] = [{'type': "success", 'message':_(u"Calibre-Web configuration updated")}]
|
||||
resp['reboot'] = reboot
|
||||
resp['config_upload']= config.config_upload_formats
|
||||
return Response(json.dumps(resp), mimetype='application/json')
|
||||
|
||||
|
||||
def _configuration_result(error_flash=None, gdrive_error=None, configured=True):
|
||||
def _db_configuration_result(error_flash=None, gdrive_error=None):
|
||||
gdrive_authenticate = not is_gdrive_ready()
|
||||
gdrivefolders = []
|
||||
if gdrive_error is None:
|
||||
if not gdrive_error and config.config_use_google_drive:
|
||||
gdrive_error = gdriveutils.get_error_text()
|
||||
if gdrive_error and gdrive_support:
|
||||
log.error(gdrive_error)
|
||||
gdrive_error = _(gdrive_error)
|
||||
flash(gdrive_error, category="error")
|
||||
else:
|
||||
if not gdrive_authenticate and gdrive_support:
|
||||
gdrivefolders = gdriveutils.listRootFolders()
|
||||
|
||||
show_back_button = current_user.is_authenticated
|
||||
show_login_button = config.db_configured and not current_user.is_authenticated
|
||||
if error_flash:
|
||||
log.error(error_flash)
|
||||
config.load()
|
||||
flash(error_flash, category="error")
|
||||
show_login_button = False
|
||||
elif request.method == "POST" and not gdrive_error:
|
||||
flash(_("Database Settings updated"), category="success")
|
||||
|
||||
return render_title_template("config_edit.html",
|
||||
return render_title_template("config_db.html",
|
||||
config=config,
|
||||
provider=oauthblueprints,
|
||||
show_back_button=show_back_button,
|
||||
show_login_button=show_login_button,
|
||||
show_authenticate_google_drive=gdrive_authenticate,
|
||||
filepicker=configured,
|
||||
gdriveError=gdrive_error,
|
||||
gdrivefolders=gdrivefolders,
|
||||
feature_support=feature_support,
|
||||
title=_(u"Basic Configuration"), page="config")
|
||||
title=_(u"Database Configuration"), page="dbconfig")
|
||||
|
||||
|
||||
def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||
@@ -1309,7 +1378,9 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||
raise Exception(_(u"E-mail is not from valid domain"))
|
||||
except Exception as ex:
|
||||
flash(str(ex), category="error")
|
||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||
return render_title_template("user_edit.html", new_user=1, content=content,
|
||||
config=config,
|
||||
translations=translations,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||
try:
|
||||
@@ -1317,6 +1388,8 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||
content.denied_tags = config.config_denied_tags
|
||||
content.allowed_column_value = config.config_allowed_column_value
|
||||
content.denied_column_value = config.config_denied_column_value
|
||||
# No default value for kobo sync shelf setting
|
||||
content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on"
|
||||
ub.session.add(content)
|
||||
ub.session.commit()
|
||||
flash(_(u"User '%(user)s' created", user=content.name), category="success")
|
||||
@@ -1335,16 +1408,32 @@ def _delete_user(content):
|
||||
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||
ub.User.id != content.id).count():
|
||||
if content.name != "Guest":
|
||||
# Delete all books in shelfs belonging to user, all shelfs of user, downloadstat of user, read status
|
||||
# and user itself
|
||||
ub.session.query(ub.ReadBook).filter(content.id == ub.ReadBook.user_id).delete()
|
||||
ub.session.query(ub.Downloads).filter(content.id == ub.Downloads.user_id).delete()
|
||||
for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id):
|
||||
ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete()
|
||||
ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id).delete()
|
||||
ub.session.query(ub.Bookmark).filter(content.id == ub.Bookmark.user_id).delete()
|
||||
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
|
||||
ub.session.query(ub.ArchivedBook).filter(ub.ArchivedBook.user_id == content.id).delete()
|
||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == content.id).delete()
|
||||
ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.user_id == content.id).delete()
|
||||
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == content.id).delete()
|
||||
# delete KoboReadingState and all it's children
|
||||
kobo_entries = ub.session.query(ub.KoboReadingState).filter(ub.KoboReadingState.user_id == content.id).all()
|
||||
for kobo_entry in kobo_entries:
|
||||
ub.session.delete(kobo_entry)
|
||||
ub.session_commit()
|
||||
log.info(u"User {} deleted".format(content.name))
|
||||
return(_(u"User '%(nick)s' deleted", nick=content.name))
|
||||
log.info("User {} deleted".format(content.name))
|
||||
return(_("User '%(nick)s' deleted", nick=content.name))
|
||||
else:
|
||||
log.warning(_(u"Can't delete Guest User"))
|
||||
raise Exception(_(u"Can't delete Guest User"))
|
||||
log.warning(_("Can't delete Guest User"))
|
||||
raise Exception(_("Can't delete Guest User"))
|
||||
else:
|
||||
log.warning(u"No admin user remaining, can't delete user")
|
||||
raise Exception(_(u"No admin user remaining, can't delete user"))
|
||||
log.warning("No admin user remaining, can't delete user")
|
||||
raise Exception(_("No admin user remaining, can't delete user"))
|
||||
|
||||
|
||||
def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||
@@ -1384,6 +1473,13 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||
else:
|
||||
content.sidebar_view &= ~constants.DETAIL_RANDOM
|
||||
|
||||
old_state = content.kobo_only_shelves_sync
|
||||
content.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0
|
||||
# 1 -> 0: nothing has to be done
|
||||
# 0 -> 1: all synced books have to be added to archived books, + currently synced shelfs
|
||||
# which don't have to be synced have to be removed (added to Shelf archive)
|
||||
if old_state == 0 and content.kobo_only_shelves_sync == 1:
|
||||
kobo_sync_status.update_on_sync_shelfs(content.id)
|
||||
if to_save.get("default_language"):
|
||||
content.default_language = to_save["default_language"]
|
||||
if to_save.get("locale"):
|
||||
@@ -1408,6 +1504,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||
kobo_support=kobo_support,
|
||||
new_user=0,
|
||||
content=content,
|
||||
config=config,
|
||||
registered_oauth=oauth_check,
|
||||
title=_(u"Edit User %(nick)s", nick=content.name),
|
||||
page="edituser")
|
||||
@@ -1439,7 +1536,10 @@ def new_user():
|
||||
else:
|
||||
content.role = config.config_default_role
|
||||
content.sidebar_view = config.config_default_show
|
||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||
content.locale = config.config_default_locale
|
||||
content.default_language = config.config_default_language
|
||||
return render_title_template("user_edit.html", new_user=1, content=content,
|
||||
config=config, translations=translations,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||
|
||||
@@ -1514,7 +1614,7 @@ def edit_user(user_id):
|
||||
if not content or (not config.config_anonbrowse and content.name == "Guest"):
|
||||
flash(_(u"User not found"), category="error")
|
||||
return redirect(url_for('admin.admin'))
|
||||
languages = calibre_db.speaking_language()
|
||||
languages = calibre_db.speaking_language(return_all_languages=True)
|
||||
translations = babel.list_translations() + [LC('en')]
|
||||
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||
if request.method == "POST":
|
||||
@@ -1527,6 +1627,7 @@ def edit_user(user_id):
|
||||
languages=languages,
|
||||
new_user=0,
|
||||
content=content,
|
||||
config=config,
|
||||
registered_oauth=oauth_check,
|
||||
mail_configured=config.get_mail_server_configured(),
|
||||
kobo_support=kobo_support,
|
||||
@@ -1534,7 +1635,7 @@ def edit_user(user_id):
|
||||
page="edituser")
|
||||
|
||||
|
||||
@admi.route("/admin/resetpassword/<int:user_id>")
|
||||
@admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def reset_user_password(user_id):
|
||||
@@ -1636,7 +1737,8 @@ def get_updater_status():
|
||||
"9": _(u'Update failed:') + u' ' + _(u'Connection error'),
|
||||
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
|
||||
"11": _(u'Update failed:') + u' ' + _(u'General error'),
|
||||
"12": _(u'Update failed:') + u' ' + _(u'Update File Could Not be Saved in Temp Dir')
|
||||
"12": _(u'Update failed:') + u' ' + _(u'Update file could not be saved in temp dir'),
|
||||
"13": _(u'Update failed:') + u' ' + _(u'Files could not be replaced during update')
|
||||
}
|
||||
status['text'] = text
|
||||
updater_thread.status = 0
|
||||
@@ -1656,7 +1758,13 @@ def get_updater_status():
|
||||
def ldap_import_create_user(user, user_data):
|
||||
user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object)
|
||||
|
||||
username = user_data[user_login_field][0].decode('utf-8')
|
||||
try:
|
||||
username = user_data[user_login_field][0].decode('utf-8')
|
||||
except KeyError as ex:
|
||||
log.error("Failed to extract LDAP user: %s - %s", user, ex)
|
||||
message = _(u'Failed to extract at least One LDAP User')
|
||||
return 0, message
|
||||
|
||||
# check for duplicate username
|
||||
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first():
|
||||
# if ub.session.query(ub.User).filter(ub.User.name == username).first():
|
||||
@@ -1684,6 +1792,8 @@ def ldap_import_create_user(user, user_data):
|
||||
content.password = '' # dummy password which will be replaced by ldap one
|
||||
content.email = useremail
|
||||
content.kindle_mail = kindlemail
|
||||
content.default_language = config.config_default_language
|
||||
content.locale = config.config_default_locale
|
||||
content.role = config.config_default_role
|
||||
content.sidebar_view = config.config_default_show
|
||||
content.allowed_tags = config.config_allowed_tags
|
||||
@@ -1701,7 +1811,7 @@ def ldap_import_create_user(user, user_data):
|
||||
return 0, message
|
||||
|
||||
|
||||
@admi.route('/import_ldap_users')
|
||||
@admi.route('/import_ldap_users', methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def import_ldap_users():
|
||||
@@ -1754,7 +1864,7 @@ def import_ldap_users():
|
||||
|
||||
|
||||
def extract_user_data_from_field(user, field):
|
||||
match = re.search(field + r"=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE)
|
||||
match = re.search(field + r"=([\.\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
else:
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
|
||||
# Uses query strings so CSS font files are found without having to resort to absolute URLs
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
|
||||
+6
-17
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
@@ -45,22 +44,10 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
|
||||
version=version_info())
|
||||
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('-f', action='store_true', help='Enables filepicker in unconfigured mode')
|
||||
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
|
||||
parser.add_argument('-l', action='store_true', help='Allow loading covers from localhost')
|
||||
args = parser.parse_args()
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
if args.p:
|
||||
args.p = args.p.decode('utf-8')
|
||||
if args.g:
|
||||
args.g = args.g.decode('utf-8')
|
||||
if args.k:
|
||||
args.k = args.k.decode('utf-8')
|
||||
if args.c:
|
||||
args.c = args.c.decode('utf-8')
|
||||
if args.s:
|
||||
args.s = args.s.decode('utf-8')
|
||||
|
||||
|
||||
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
|
||||
gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db")
|
||||
|
||||
@@ -91,6 +78,8 @@ if (args.k and not args.c) or (not args.k and args.c):
|
||||
if args.k == "":
|
||||
keyfilepath = ""
|
||||
|
||||
# load covers from localhost
|
||||
allow_localhost = args.l or None
|
||||
# handle and check ip address argument
|
||||
ip_address = args.i or None
|
||||
if ip_address:
|
||||
@@ -115,5 +104,5 @@ if user_credentials and ":" not in user_credentials:
|
||||
print("No valid 'username:password' format")
|
||||
sys.exit(3)
|
||||
|
||||
# Handles enabling of filepicker
|
||||
filepicker = args.f or None
|
||||
if args.f:
|
||||
print("Warning: -f flag is depreciated and will be removed in next version")
|
||||
|
||||
+14
-15
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
|
||||
from . import logger, isoLanguages
|
||||
@@ -57,25 +56,25 @@ COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
|
||||
|
||||
def _cover_processing(tmp_file_name, img, extension):
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
|
||||
if use_IM:
|
||||
# convert to jpg because calibre only supports jpg
|
||||
if extension in NO_JPEG_EXTENSIONS:
|
||||
with Image(filename=tmp_file_name) as imgc:
|
||||
if extension in NO_JPEG_EXTENSIONS:
|
||||
if use_IM:
|
||||
with Image(blob=img) as imgc:
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace('rgb')
|
||||
imgc.save(tmp_cover_name)
|
||||
imgc.save(filename=tmp_cover_name)
|
||||
return tmp_cover_name
|
||||
|
||||
if not img:
|
||||
else:
|
||||
return None
|
||||
if img:
|
||||
with open(tmp_cover_name, 'wb') as f:
|
||||
f.write(img)
|
||||
return tmp_cover_name
|
||||
else:
|
||||
return None
|
||||
|
||||
with open(tmp_cover_name, 'wb') as f:
|
||||
f.write(img)
|
||||
return tmp_cover_name
|
||||
|
||||
|
||||
def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable):
|
||||
cover_data = None
|
||||
cover_data = extension = None
|
||||
if original_file_extension.upper() == '.CBZ':
|
||||
cf = zipfile.ZipFile(tmp_file_name)
|
||||
for name in cf.namelist():
|
||||
@@ -107,7 +106,7 @@ def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecu
|
||||
break
|
||||
except Exception as ex:
|
||||
log.debug('Rarfile failed with error: %s', ex)
|
||||
return cover_data
|
||||
return cover_data, extension
|
||||
|
||||
|
||||
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||
@@ -122,7 +121,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||
cover_data = archive.getPage(index)
|
||||
break
|
||||
else:
|
||||
cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
|
||||
cover_data, extension = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
|
||||
return _cover_processing(tmp_file_name, cover_data, extension)
|
||||
|
||||
|
||||
|
||||
+5
-8
@@ -16,8 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
@@ -68,7 +66,7 @@ class _Settings(_Base):
|
||||
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
|
||||
config_certfile = Column(String)
|
||||
config_keyfile = Column(String)
|
||||
|
||||
config_trustedhosts = Column(String,default='')
|
||||
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
||||
config_books_per_page = Column(Integer, default=60)
|
||||
config_random_books = Column(Integer, default=4)
|
||||
@@ -91,6 +89,8 @@ class _Settings(_Base):
|
||||
|
||||
config_default_role = Column(SmallInteger, default=0)
|
||||
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
|
||||
config_default_language = Column(String(3), default="all")
|
||||
config_default_locale = Column(String(2), default="en")
|
||||
config_columns_to_ignore = Column(String)
|
||||
|
||||
config_denied_tags = Column(String, default="")
|
||||
@@ -133,6 +133,7 @@ class _Settings(_Base):
|
||||
config_calibre = Column(String)
|
||||
config_rarfile_location = Column(String, default=None)
|
||||
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
|
||||
config_unicode_filename =Column(Boolean, default=False)
|
||||
|
||||
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
||||
|
||||
@@ -347,7 +348,7 @@ class _ConfigSQL(object):
|
||||
log.error(error)
|
||||
log.warning("invalidating configuration")
|
||||
self.db_configured = False
|
||||
self.config_calibre_dir = None
|
||||
# self.config_calibre_dir = None
|
||||
self.save()
|
||||
|
||||
|
||||
@@ -360,10 +361,6 @@ def _migrate_table(session, orm_class):
|
||||
session.query(column).first()
|
||||
except OperationalError as err:
|
||||
log.debug("%s: %s", column_name, err.args[0])
|
||||
if column.default is not None:
|
||||
if sys.version_info < (3, 0):
|
||||
if isinstance(column.default.arg, unicode):
|
||||
column.default.arg = column.default.arg.encode('utf-8')
|
||||
if column.default is None:
|
||||
column_default = ""
|
||||
else:
|
||||
|
||||
+5
-8
@@ -16,10 +16,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
from collections import namedtuple
|
||||
from sqlalchemy import __version__ as sql_version
|
||||
|
||||
sqlalchemy_version2 = ([int(x) for x in sql_version.split('.')] >= [2,0,0])
|
||||
|
||||
# if installed via pip this variable is set to true (empty file with name .HOMEDIR present)
|
||||
HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR'))
|
||||
@@ -28,12 +30,7 @@ HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file
|
||||
UPDATER_AVAILABLE = True
|
||||
|
||||
# Base dir is parent of current file, necessary if called from different folder
|
||||
if sys.version_info < (3, 0):
|
||||
BASE_DIR = os.path.abspath(os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),os.pardir)).decode('utf-8')
|
||||
else:
|
||||
BASE_DIR = os.path.abspath(os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),os.pardir))
|
||||
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),os.pardir))
|
||||
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
||||
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
||||
@@ -154,7 +151,7 @@ def selected_roles(dictionary):
|
||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||
'series_id, languages, publisher')
|
||||
|
||||
STABLE_VERSION = {'version': '0.6.12'}
|
||||
STABLE_VERSION = {'version': '0.6.16'}
|
||||
|
||||
NIGHTLY_VERSION = {}
|
||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||
|
||||
+3
-4
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import re
|
||||
from flask_babel import gettext as _
|
||||
@@ -39,9 +38,9 @@ def _get_command_version(path, pattern, argument=None):
|
||||
if argument:
|
||||
command.append(argument)
|
||||
try:
|
||||
for line in process_wait(command):
|
||||
if re.search(pattern, line):
|
||||
return line
|
||||
match = process_wait(command, pattern=pattern)
|
||||
if isinstance(match, re.Match):
|
||||
return match.string
|
||||
except Exception as ex:
|
||||
log.warning("%s: %s", path, ex)
|
||||
return _EXECUTION_ERROR
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import ast
|
||||
import json
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
||||
@@ -167,6 +167,8 @@ class Identifiers(Base):
|
||||
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 self.val.lower().startswith("javascript:"):
|
||||
return quote(self.val)
|
||||
else:
|
||||
return u"{0}".format(self.val)
|
||||
|
||||
@@ -175,8 +177,8 @@ class Comments(Base):
|
||||
__tablename__ = 'comments'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
|
||||
text = Column(String(collation='NOCASE'), nullable=False)
|
||||
book = Column(Integer, ForeignKey('books.id'), nullable=False)
|
||||
|
||||
def __init__(self, text, book):
|
||||
self.text = text
|
||||
@@ -387,8 +389,6 @@ class Custom_Columns(Base):
|
||||
|
||||
def get_display_dict(self):
|
||||
display_dict = ast.literal_eval(self.display)
|
||||
if sys.version_info < (3, 0):
|
||||
display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']]
|
||||
return display_dict
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@ class AlchemyEncoder(json.JSONEncoder):
|
||||
if isinstance(o.__class__, DeclarativeMeta):
|
||||
# an SQLAlchemy class
|
||||
fields = {}
|
||||
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x!="password"]:
|
||||
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x != "password"]:
|
||||
if field == 'books':
|
||||
continue
|
||||
data = o.__getattribute__(field)
|
||||
@@ -407,8 +407,11 @@ class AlchemyEncoder(json.JSONEncoder):
|
||||
data = data.replace("'", "\'")
|
||||
elif isinstance(data, InstrumentedList):
|
||||
el = list()
|
||||
# ele = None
|
||||
for ele in data:
|
||||
if ele.get:
|
||||
if hasattr(ele, 'value'): # converter for custom_column values
|
||||
el.append(str(ele.value))
|
||||
elif ele.get:
|
||||
el.append(ele.get())
|
||||
else:
|
||||
el.append(json.dumps(ele, cls=AlchemyEncoder))
|
||||
@@ -524,19 +527,41 @@ class CalibreDB():
|
||||
return cc_classes
|
||||
|
||||
@classmethod
|
||||
def setup_db(cls, config, app_db_path):
|
||||
def check_valid_db(cls, config_calibre_dir, app_db_path):
|
||||
if not config_calibre_dir:
|
||||
return False
|
||||
dbpath = os.path.join(config_calibre_dir, "metadata.db")
|
||||
if not os.path.exists(dbpath):
|
||||
return False
|
||||
try:
|
||||
check_engine = create_engine('sqlite://',
|
||||
echo=False,
|
||||
isolation_level="SERIALIZABLE",
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=StaticPool)
|
||||
with check_engine.begin() as connection:
|
||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
||||
check_engine.connect()
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def update_config(cls, config):
|
||||
cls.config = config
|
||||
|
||||
@classmethod
|
||||
def setup_db(cls, config_calibre_dir, app_db_path):
|
||||
cls.dispose()
|
||||
|
||||
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
|
||||
|
||||
if not config.config_calibre_dir:
|
||||
config.invalidate()
|
||||
if not config_calibre_dir:
|
||||
cls.config.invalidate()
|
||||
return False
|
||||
|
||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||
dbpath = os.path.join(config_calibre_dir, "metadata.db")
|
||||
if not os.path.exists(dbpath):
|
||||
config.invalidate()
|
||||
cls.config.invalidate()
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -552,14 +577,14 @@ class CalibreDB():
|
||||
conn = cls.engine.connect()
|
||||
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
||||
except Exception as ex:
|
||||
config.invalidate(ex)
|
||||
cls.config.invalidate(ex)
|
||||
return False
|
||||
|
||||
config.db_configured = True
|
||||
cls.config.db_configured = True
|
||||
|
||||
if not cc_classes:
|
||||
try:
|
||||
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
||||
cc = conn.execute(text("SELECT id, datatype FROM custom_columns"))
|
||||
cls.setup_db_cc_classes(cc)
|
||||
except OperationalError as e:
|
||||
log.debug_or_exception(e)
|
||||
@@ -587,7 +612,7 @@ class CalibreDB():
|
||||
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
|
||||
|
||||
# Language and content filters for displaying in the UI
|
||||
def common_filters(self, allow_show_archived=False):
|
||||
def common_filters(self, allow_show_archived=False, return_all_languages=False):
|
||||
if not allow_show_archived:
|
||||
archived_books = (
|
||||
ub.session.query(ub.ArchivedBook)
|
||||
@@ -600,10 +625,10 @@ class CalibreDB():
|
||||
else:
|
||||
archived_filter = true()
|
||||
|
||||
if current_user.filter_language() != "all":
|
||||
lang_filter = Books.languages.any(Languages.lang_code == current_user.filter_language())
|
||||
else:
|
||||
if current_user.filter_language() == "all" or return_all_languages:
|
||||
lang_filter = true()
|
||||
else:
|
||||
lang_filter = Books.languages.any(Languages.lang_code == current_user.filter_language())
|
||||
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))
|
||||
@@ -665,6 +690,12 @@ class CalibreDB():
|
||||
randm = false()
|
||||
off = int(int(pagesize) * (page - 1))
|
||||
query = self.session.query(database)
|
||||
if len(join) == 6:
|
||||
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
|
||||
if len(join) == 5:
|
||||
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4])
|
||||
if len(join) == 4:
|
||||
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3])
|
||||
if len(join) == 3:
|
||||
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
|
||||
elif len(join) == 2:
|
||||
@@ -709,6 +740,7 @@ class CalibreDB():
|
||||
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=escape(r.name.replace(*replace))) for r in entries])
|
||||
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
||||
return json_dumps
|
||||
|
||||
@@ -730,6 +762,8 @@ class CalibreDB():
|
||||
for authorterm in authorterms:
|
||||
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
|
||||
query = self.session.query(Books)
|
||||
if len(join) == 6:
|
||||
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
|
||||
if len(join) == 3:
|
||||
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
|
||||
elif len(join) == 2:
|
||||
@@ -746,7 +780,7 @@ class CalibreDB():
|
||||
|
||||
# read search results from calibre-database and return it (function is used for feed and simple search
|
||||
def get_search_results(self, term, offset=None, order=None, limit=None, *join):
|
||||
order = order or [Books.sort]
|
||||
order = order[0] if order else [Books.sort]
|
||||
pagination = None
|
||||
result = self.search_query(term, *join).order_by(*order).all()
|
||||
result_count = len(result)
|
||||
@@ -762,22 +796,29 @@ class CalibreDB():
|
||||
return result[offset:limit_all], result_count, pagination
|
||||
|
||||
# Creates for all stored languages a translated speaking name in the array for the UI
|
||||
def speaking_language(self, languages=None):
|
||||
def speaking_language(self, languages=None, return_all_languages=False, with_count=False, reverse_order=False):
|
||||
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
|
||||
if with_count:
|
||||
if not languages:
|
||||
languages = self.session.query(Languages, func.count('books_languages_link.book'))\
|
||||
.join(books_languages_link).join(Books)\
|
||||
.filter(self.common_filters(return_all_languages=return_all_languages)) \
|
||||
.group_by(text('books_languages_link.lang_code')).all()
|
||||
for lang in languages:
|
||||
lang[0].name = isoLanguages.get_language_name(get_locale(), lang[0].lang_code)
|
||||
return sorted(languages, key=lambda x: x[0].name, reverse=reverse_order)
|
||||
else:
|
||||
if not languages:
|
||||
languages = self.session.query(Languages) \
|
||||
.join(books_languages_link) \
|
||||
.join(Books) \
|
||||
.filter(self.common_filters(return_all_languages=return_all_languages)) \
|
||||
.group_by(text('books_languages_link.lang_code')).all()
|
||||
for lang in languages:
|
||||
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||
return sorted(languages, key=lambda x: x.name, reverse=reverse_order)
|
||||
|
||||
|
||||
def update_title_sort(self, config, conn=None):
|
||||
# user defined sort function for calibre databases (Series, etc.)
|
||||
@@ -828,7 +869,8 @@ class CalibreDB():
|
||||
def reconnect_db(self, config, app_db_path):
|
||||
self.dispose()
|
||||
self.engine.dispose()
|
||||
self.setup_db(config, app_db_path)
|
||||
self.setup_db(config.config_calibre_dir, app_db_path)
|
||||
self.update_config(config)
|
||||
|
||||
|
||||
def lcase(s):
|
||||
|
||||
+20
-13
@@ -22,14 +22,10 @@ import glob
|
||||
import zipfile
|
||||
import json
|
||||
from io import BytesIO
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
|
||||
import os
|
||||
|
||||
from flask import send_file
|
||||
from flask import send_file, __version__
|
||||
|
||||
from . import logger, config
|
||||
from .about import collect_stats
|
||||
@@ -38,14 +34,20 @@ log = logger.create()
|
||||
|
||||
def assemble_logfiles(file_name):
|
||||
log_list = sorted(glob.glob(file_name + '*'), reverse=True)
|
||||
wfd = StringIO()
|
||||
wfd = BytesIO()
|
||||
for f in log_list:
|
||||
with open(f, 'r') as fd:
|
||||
with open(f, 'rb') as fd:
|
||||
shutil.copyfileobj(fd, wfd)
|
||||
wfd.seek(0)
|
||||
return send_file(wfd,
|
||||
as_attachment=True,
|
||||
attachment_filename=os.path.basename(file_name))
|
||||
if int(__version__.split('.')[0]) < 2:
|
||||
return send_file(wfd,
|
||||
as_attachment=True,
|
||||
attachment_filename=os.path.basename(file_name))
|
||||
else:
|
||||
return send_file(wfd,
|
||||
as_attachment=True,
|
||||
download_name=os.path.basename(file_name))
|
||||
|
||||
|
||||
def send_debug():
|
||||
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
|
||||
@@ -60,6 +62,11 @@ def send_debug():
|
||||
for fp in file_list:
|
||||
zf.write(fp, os.path.basename(fp))
|
||||
memory_zip.seek(0)
|
||||
return send_file(memory_zip,
|
||||
as_attachment=True,
|
||||
attachment_filename="Calibre-Web-debug-pack.zip")
|
||||
if int(__version__.split('.')[0]) < 2:
|
||||
return send_file(memory_zip,
|
||||
as_attachment=True,
|
||||
attachment_filename="Calibre-Web-debug-pack.zip")
|
||||
else:
|
||||
return send_file(memory_zip,
|
||||
as_attachment=True,
|
||||
download_name="Calibre-Web-debug-pack.zip")
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from .constants import BASE_DIR
|
||||
try:
|
||||
from importlib_metadata import version
|
||||
importlib = True
|
||||
ImportNotFound = BaseException
|
||||
except ImportError:
|
||||
importlib = False
|
||||
|
||||
|
||||
if not importlib:
|
||||
try:
|
||||
import pkg_resources
|
||||
from pkg_resources import DistributionNotFound as ImportNotFound
|
||||
pkgresources = True
|
||||
except ImportError as e:
|
||||
pkgresources = False
|
||||
|
||||
def load_dependencys(optional=False):
|
||||
deps = list()
|
||||
if importlib or pkgresources:
|
||||
if optional:
|
||||
req_path = os.path.join(BASE_DIR, "optional-requirements.txt")
|
||||
else:
|
||||
req_path = os.path.join(BASE_DIR, "requirements.txt")
|
||||
if os.path.exists(req_path):
|
||||
with open(req_path, 'r') as f:
|
||||
for line in f:
|
||||
if not line.startswith('#') and not line == '\n' and not line.startswith('git'):
|
||||
res = re.match(r'(.*?)([<=>\s]+)([\d\.]+),?\s?([<=>\s]+)?([\d\.]+)?', line.strip())
|
||||
try:
|
||||
if importlib:
|
||||
dep_version = version(res.group(1))
|
||||
else:
|
||||
dep_version = pkg_resources.get_distribution(res.group(1)).version
|
||||
except ImportNotFound:
|
||||
if optional:
|
||||
continue
|
||||
dep_version = "not installed"
|
||||
deps.append([dep_version, res.group(1), res.group(2), res.group(3), res.group(4), res.group(5)])
|
||||
return deps
|
||||
|
||||
|
||||
def dependency_check(optional=False):
|
||||
d = list()
|
||||
deps = load_dependencys(optional)
|
||||
for dep in deps:
|
||||
try:
|
||||
dep_version_int = [int(x) for x in dep[0].split('.')]
|
||||
low_check = [int(x) for x in dep[3].split('.')]
|
||||
high_check = [int(x) for x in dep[5].split('.')]
|
||||
except AttributeError:
|
||||
high_check = None
|
||||
except ValueError:
|
||||
d.append({'name': dep[1],
|
||||
'target': "available",
|
||||
'found': "Not available"
|
||||
})
|
||||
continue
|
||||
|
||||
if dep[2].strip() == "==":
|
||||
if dep_version_int != low_check:
|
||||
d.append({'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[2] + dep[3]})
|
||||
continue
|
||||
elif dep[2].strip() == ">=":
|
||||
if dep_version_int < low_check:
|
||||
d.append({'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[2] + dep[3]})
|
||||
continue
|
||||
elif dep[2].strip() == ">":
|
||||
if dep_version_int <= low_check:
|
||||
d.append({'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[2] + dep[3]})
|
||||
continue
|
||||
if dep[4] and dep[5]:
|
||||
if dep[4].strip() == "<":
|
||||
if dep_version_int >= high_check:
|
||||
d.append(
|
||||
{'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[4] + dep[5]})
|
||||
continue
|
||||
elif dep[4].strip() == "<=":
|
||||
if dep_version_int > high_check:
|
||||
d.append(
|
||||
{'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[4] + dep[5]})
|
||||
continue
|
||||
return d
|
||||
Regular → Executable
+147
-42
@@ -20,21 +20,32 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
from datetime import datetime
|
||||
import json
|
||||
from shutil import copyfile
|
||||
from uuid import uuid4
|
||||
from markupsafe import escape
|
||||
from functools import wraps
|
||||
|
||||
try:
|
||||
from lxml.html.clean import clean_html
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Improve this to check if scholarly is available in a global way, like other pythonic libraries
|
||||
try:
|
||||
from scholarly import scholarly
|
||||
have_scholar = True
|
||||
except ImportError:
|
||||
have_scholar = False
|
||||
|
||||
from babel import Locale as LC
|
||||
from babel.core import UnknownLocaleError
|
||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.exc import OperationalError, IntegrityError
|
||||
from sqlite3 import OperationalError as sqliteOperationalError
|
||||
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper
|
||||
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
|
||||
from . import config, get_locale, ub, db
|
||||
from . import calibre_db
|
||||
from .services.worker import WorkerThread
|
||||
@@ -42,11 +53,6 @@ from .tasks.upload import TaskUpload
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError:
|
||||
pass # We're not using Python 3
|
||||
|
||||
|
||||
editbook = Blueprint('editbook', __name__)
|
||||
log = logger.create()
|
||||
@@ -55,7 +61,7 @@ log = logger.create()
|
||||
def upload_required(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
if current_user.role_upload() or current_user.role_admin():
|
||||
if current_user.role_upload():
|
||||
return f(*args, **kwargs)
|
||||
abort(403)
|
||||
|
||||
@@ -226,17 +232,17 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
|
||||
changed = True
|
||||
return changed, error
|
||||
|
||||
@editbook.route("/ajax/delete/<int:book_id>")
|
||||
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_book_from_details(book_id):
|
||||
return Response(delete_book(book_id, "", True), mimetype='application/json')
|
||||
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
|
||||
|
||||
|
||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""})
|
||||
@editbook.route("/delete/<int:book_id>/<string:book_format>")
|
||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
||||
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_book_ajax(book_id, book_format):
|
||||
return delete_book(book_id, book_format, False)
|
||||
return delete_book_from_table(book_id, book_format, False)
|
||||
|
||||
|
||||
def delete_whole_book(book_id, book):
|
||||
@@ -306,7 +312,7 @@ def render_delete_book_result(book_format, jsonResponse, warning, book_id):
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
|
||||
def delete_book(book_id, book_format, jsonResponse):
|
||||
def delete_book_from_table(book_id, book_format, jsonResponse):
|
||||
warning = {}
|
||||
if current_user.role_delete_books():
|
||||
book = calibre_db.get_book(book_id)
|
||||
@@ -335,6 +341,8 @@ def delete_book(book_id, book_format, jsonResponse):
|
||||
else:
|
||||
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
|
||||
filter(db.Data.format == book_format).delete()
|
||||
if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']:
|
||||
kobo_sync_status.remove_synced_book(book.id, True)
|
||||
calibre_db.session.commit()
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
@@ -351,7 +359,16 @@ def delete_book(book_id, book_format, jsonResponse):
|
||||
else:
|
||||
# book not found
|
||||
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
||||
return render_delete_book_result(book_format, jsonResponse, warning, book_id)
|
||||
return render_delete_book_result(book_format, jsonResponse, warning, book_id)
|
||||
message = _("You are missing permissions to delete books")
|
||||
if jsonResponse:
|
||||
return json.dumps({"location": url_for("editbook.edit_book", book_id=book_id),
|
||||
"type": "danger",
|
||||
"format": "",
|
||||
"message": message})
|
||||
else:
|
||||
flash(message, category="error")
|
||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||
|
||||
|
||||
def render_edit_book(book_id):
|
||||
@@ -442,14 +459,17 @@ def edit_book_series_index(series_index, book):
|
||||
if not series_index.replace('.', '', 1).isdigit():
|
||||
flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
|
||||
return False
|
||||
if book.series_index != series_index:
|
||||
if str(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 comments:
|
||||
comments = clean_html(comments)
|
||||
if len(book.comments):
|
||||
if book.comments[0].text != comments:
|
||||
book.comments[0].text = comments
|
||||
@@ -469,11 +489,11 @@ def edit_book_languages(languages, book, upload=False, invalid=None):
|
||||
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)
|
||||
log.error("'%s' is not a valid language", l)
|
||||
if isinstance(invalid, list):
|
||||
invalid.append(l)
|
||||
else:
|
||||
flash(_(u"%(langname)s is not a valid language", langname=l), category="warning")
|
||||
raise ValueError(_(u"'%(langname)s' is not a valid language", langname=l))
|
||||
# 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
|
||||
@@ -488,7 +508,7 @@ def edit_book_languages(languages, book, upload=False, invalid=None):
|
||||
|
||||
def edit_book_publisher(publishers, book):
|
||||
changed = False
|
||||
if publishers:
|
||||
if publishers:
|
||||
publisher = publishers.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,
|
||||
@@ -506,6 +526,8 @@ def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string):
|
||||
to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
|
||||
elif c.datatype == 'comments':
|
||||
to_save[cc_string] = Markup(to_save[cc_string]).unescape()
|
||||
if to_save[cc_string]:
|
||||
to_save[cc_string] = clean_html(to_save[cc_string])
|
||||
elif c.datatype == 'datetime':
|
||||
try:
|
||||
to_save[cc_string] = datetime.strptime(to_save[cc_string], "%Y-%m-%d")
|
||||
@@ -557,10 +579,19 @@ def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
|
||||
getattr(book, cc_string).append(new_cc)
|
||||
return changed, to_save
|
||||
|
||||
def edit_single_cc_data(book_id, book, column_id, to_save):
|
||||
cc = (calibre_db.session.query(db.Custom_Columns)
|
||||
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions))
|
||||
.filter(db.Custom_Columns.id == column_id)
|
||||
.all())
|
||||
return edit_cc_data(book_id, book, to_save, cc)
|
||||
|
||||
def edit_cc_data(book_id, book, to_save):
|
||||
changed = False
|
||||
def edit_all_cc_data(book_id, book, to_save):
|
||||
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||
return edit_cc_data(book_id, book, to_save, cc)
|
||||
|
||||
def edit_cc_data(book_id, book, to_save, cc):
|
||||
changed = False
|
||||
for c in cc:
|
||||
cc_string = "custom_column_" + str(c.id)
|
||||
if not c.is_multiple:
|
||||
@@ -645,9 +676,9 @@ def upload_single_file(request, book, book_id):
|
||||
return redirect(url_for('web.show_book', book_id=book.id))
|
||||
|
||||
# Queue uploader info
|
||||
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
|
||||
WorkerThread.add(current_user.name, TaskUpload(
|
||||
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>"))
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
|
||||
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
|
||||
WorkerThread.add(current_user.name, TaskUpload(uploadText))
|
||||
|
||||
return uploader.process(
|
||||
saved_filename, *os.path.splitext(requested_file.filename),
|
||||
@@ -795,7 +826,7 @@ def edit_book(book_id):
|
||||
# handle book ratings
|
||||
modif_date |= edit_book_ratings(to_save, book)
|
||||
# handle cc data
|
||||
modif_date |= edit_cc_data(book_id, book, to_save)
|
||||
modif_date |= edit_all_cc_data(book_id, book, to_save)
|
||||
|
||||
if to_save["pubdate"]:
|
||||
try:
|
||||
@@ -807,6 +838,8 @@ def edit_book(book_id):
|
||||
|
||||
if modif_date:
|
||||
book.last_modified = datetime.utcnow()
|
||||
kobo_sync_status.remove_synced_book(edited_books_id, all=True)
|
||||
|
||||
calibre_db.session.merge(book)
|
||||
calibre_db.session.commit()
|
||||
if config.config_use_google_drive:
|
||||
@@ -820,6 +853,10 @@ def edit_book(book_id):
|
||||
calibre_db.session.rollback()
|
||||
flash(error, category="error")
|
||||
return render_edit_book(book_id)
|
||||
except ValueError as e:
|
||||
calibre_db.session.rollback()
|
||||
flash(str(e), category="error")
|
||||
return redirect(url_for('web.show_book', book_id=book.id))
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
calibre_db.session.rollback()
|
||||
@@ -916,7 +953,11 @@ def create_book_on_upload(modif_date, meta):
|
||||
modif_date |= edit_book_series_index(meta.series_id, db_book)
|
||||
|
||||
# add languages
|
||||
modif_date |= edit_book_languages(meta.languages, db_book, upload=True)
|
||||
invalid=[]
|
||||
modif_date |= edit_book_languages(meta.languages, db_book, upload=True, invalid=invalid)
|
||||
if invalid:
|
||||
for l in invalid:
|
||||
flash(_(u"'%(langname)s' is not a valid language", langname=l), category="warning")
|
||||
|
||||
# handle tags
|
||||
modif_date |= edit_book_tags(meta.tags, db_book)
|
||||
@@ -979,7 +1020,7 @@ def move_coverfile(meta, db_book):
|
||||
category="error")
|
||||
|
||||
|
||||
@editbook.route("/upload", methods=["GET", "POST"])
|
||||
@editbook.route("/upload", methods=["POST"])
|
||||
@login_required_if_no_ano
|
||||
@upload_required
|
||||
def upload():
|
||||
@@ -1009,7 +1050,7 @@ def upload():
|
||||
config.config_calibre_dir,
|
||||
input_authors[0],
|
||||
meta.file_path,
|
||||
title_dir + meta.extension)
|
||||
title_dir + meta.extension.lower())
|
||||
|
||||
move_coverfile(meta, db_book)
|
||||
|
||||
@@ -1020,9 +1061,9 @@ def upload():
|
||||
gdriveutils.updateGdriveCalibreFromLocal()
|
||||
if error:
|
||||
flash(error, category="error")
|
||||
uploadText=_(u"File %(file)s uploaded", file=title)
|
||||
WorkerThread.add(current_user.name, TaskUpload(
|
||||
"<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>"))
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
|
||||
uploadText = _(u"File %(file)s uploaded", file=link)
|
||||
WorkerThread.add(current_user.name, TaskUpload(uploadText))
|
||||
|
||||
if len(request.files.getlist("btn-upload")) < 2:
|
||||
if current_user.role_edit() or current_user.role_admin():
|
||||
@@ -1061,6 +1102,18 @@ def convert_bookformat(book_id):
|
||||
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||
|
||||
@editbook.route("/ajax/getcustomenum/<int:c_id>")
|
||||
@login_required
|
||||
def table_get_custom_enum(c_id):
|
||||
ret = list()
|
||||
cc = (calibre_db.session.query(db.Custom_Columns)
|
||||
.filter(db.Custom_Columns.id == c_id)
|
||||
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).one_or_none())
|
||||
ret.append({'value': "", 'text': ""})
|
||||
for idx, en in enumerate(cc.get_display_dict()['enum_values']):
|
||||
ret.append({'value': en, 'text': en})
|
||||
return json.dumps(ret)
|
||||
|
||||
|
||||
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
||||
@login_required_if_no_ano
|
||||
@@ -1095,10 +1148,7 @@ def edit_list_book(param):
|
||||
else:
|
||||
lang_names = list()
|
||||
for lang in book.languages:
|
||||
try:
|
||||
lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale()))
|
||||
except UnknownLocaleError:
|
||||
lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name))
|
||||
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
|
||||
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
|
||||
mimetype='application/json')
|
||||
elif param =='author_sort':
|
||||
@@ -1115,18 +1165,33 @@ def edit_list_book(param):
|
||||
book.sort = vals['value']
|
||||
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
|
||||
mimetype='application/json')
|
||||
elif param =='comments':
|
||||
edit_book_comments(vals['value'], book)
|
||||
ret = Response(json.dumps({'success': True, 'newValue': book.comments[0].text}),
|
||||
mimetype='application/json')
|
||||
elif param =='authors':
|
||||
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
|
||||
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
|
||||
ret = Response(json.dumps({'success': True,
|
||||
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
|
||||
mimetype='application/json')
|
||||
elif param.startswith("custom_column_"):
|
||||
new_val = dict()
|
||||
new_val[param] = vals['value']
|
||||
edit_single_cc_data(book.id, book, param[14:], new_val)
|
||||
ret = Response(json.dumps({'success': True, 'newValue': vals['value']}),
|
||||
mimetype='application/json')
|
||||
|
||||
book.last_modified = datetime.utcnow()
|
||||
calibre_db.session.commit()
|
||||
# revert change for sort if automatic fields link is deactivated
|
||||
if param == 'title' and vals.get('checkT') == "false":
|
||||
book.sort = sort
|
||||
try:
|
||||
calibre_db.session.commit()
|
||||
# revert change for sort if automatic fields link is deactivated
|
||||
if param == 'title' and vals.get('checkT') == "false":
|
||||
book.sort = sort
|
||||
calibre_db.session.commit()
|
||||
except (OperationalError, IntegrityError) as e:
|
||||
calibre_db.session.rollback()
|
||||
log.error("Database error: %s", e)
|
||||
return ret
|
||||
|
||||
|
||||
@@ -1195,6 +1260,46 @@ def merge_list_book():
|
||||
element.format,
|
||||
element.uncompressed_size,
|
||||
to_name))
|
||||
delete_book(from_book.id,"", True)
|
||||
delete_book_from_table(from_book.id,"", True)
|
||||
return json.dumps({'success': True})
|
||||
return ""
|
||||
|
||||
@editbook.route("/ajax/xchange", methods=['POST'])
|
||||
@login_required
|
||||
@edit_required
|
||||
def table_xchange_author_title():
|
||||
vals = request.get_json().get('xchange')
|
||||
if vals:
|
||||
for val in vals:
|
||||
modif_date = False
|
||||
book = calibre_db.get_book(val)
|
||||
authors = book.title
|
||||
entries = calibre_db.order_authors(book)
|
||||
author_names = []
|
||||
for authr in entries.authors:
|
||||
author_names.append(authr.name.replace('|', ','))
|
||||
|
||||
title_change = handle_title_on_edit(book, " ".join(author_names))
|
||||
input_authors, authorchange = handle_author_on_edit(book, authors)
|
||||
if authorchange or title_change:
|
||||
edited_books_id = book.id
|
||||
modif_date = True
|
||||
|
||||
if config.config_use_google_drive:
|
||||
gdriveutils.updateGdriveCalibreFromLocal()
|
||||
|
||||
if edited_books_id:
|
||||
helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0])
|
||||
if modif_date:
|
||||
book.last_modified = datetime.utcnow()
|
||||
try:
|
||||
calibre_db.session.commit()
|
||||
except (OperationalError, IntegrityError) as e:
|
||||
calibre_db.session.rollback()
|
||||
log.error("Database error: %s", e)
|
||||
return json.dumps({'success': False})
|
||||
|
||||
if config.config_use_google_drive:
|
||||
gdriveutils.updateGdriveCalibreFromLocal()
|
||||
return json.dumps({'success': True})
|
||||
return ""
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import zipfile
|
||||
from lxml import etree
|
||||
|
||||
@@ -35,6 +35,7 @@ def error_http(error):
|
||||
error_code="Error {0}".format(error.code),
|
||||
error_name=error.name,
|
||||
issue=False,
|
||||
unconfigured=not config.db_configured,
|
||||
instance=config.config_calibre_web_title
|
||||
), error.code
|
||||
|
||||
@@ -44,6 +45,7 @@ def internal_error(error):
|
||||
error_code="Internal Server Error",
|
||||
error_name=str(error),
|
||||
issue=True,
|
||||
unconfigured=False,
|
||||
error_stack=traceback.format_exc().split("\n"),
|
||||
instance=config.config_calibre_web_title
|
||||
), 500
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
from lxml import etree
|
||||
|
||||
from .constants import BookMeta
|
||||
|
||||
+42
-44
@@ -20,9 +20,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import json
|
||||
import tempfile
|
||||
@@ -34,7 +32,7 @@ from flask import Blueprint, flash, request, redirect, url_for, abort
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import login_required
|
||||
|
||||
from . import logger, gdriveutils, config, ub, calibre_db
|
||||
from . import logger, gdriveutils, config, ub, calibre_db, csrf
|
||||
from .admin import admin_required
|
||||
|
||||
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||
@@ -74,7 +72,7 @@ def google_drive_callback():
|
||||
f.write(credentials.to_json())
|
||||
except (ValueError, AttributeError) as error:
|
||||
log.error(error)
|
||||
return redirect(url_for('admin.configuration'))
|
||||
return redirect(url_for('admin.db_configuration'))
|
||||
|
||||
|
||||
@gdrive.route("/watch/subscribe")
|
||||
@@ -99,7 +97,7 @@ def watch_gdrive():
|
||||
else:
|
||||
flash(reason['message'], category="error")
|
||||
|
||||
return redirect(url_for('admin.configuration'))
|
||||
return redirect(url_for('admin.db_configuration'))
|
||||
|
||||
|
||||
@gdrive.route("/watch/revoke")
|
||||
@@ -111,50 +109,50 @@ def revoke_watch_gdrive():
|
||||
try:
|
||||
gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'],
|
||||
last_watch_response['resourceId'])
|
||||
except HttpError:
|
||||
except (HttpError, AttributeError):
|
||||
pass
|
||||
config.config_google_drive_watch_changes_response = {}
|
||||
config.save()
|
||||
return redirect(url_for('admin.configuration'))
|
||||
return redirect(url_for('admin.db_configuration'))
|
||||
|
||||
try:
|
||||
@csrf.exempt
|
||||
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
||||
def on_received_watch_confirmation():
|
||||
if not config.config_google_drive_watch_changes_response:
|
||||
return ''
|
||||
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
||||
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
||||
or not request.data:
|
||||
return ''
|
||||
|
||||
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
||||
def on_received_watch_confirmation():
|
||||
if not config.config_google_drive_watch_changes_response:
|
||||
return ''
|
||||
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
||||
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
||||
or not request.data:
|
||||
return ''
|
||||
log.debug('%r', request.headers)
|
||||
log.debug('%r', request.data)
|
||||
log.info('Change received from gdrive')
|
||||
|
||||
log.debug('%r', request.headers)
|
||||
log.debug('%r', request.data)
|
||||
log.info('Change received from gdrive')
|
||||
|
||||
try:
|
||||
j = json.loads(request.data)
|
||||
log.info('Getting change details')
|
||||
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
|
||||
log.debug('%r', response)
|
||||
if response:
|
||||
if sys.version_info < (3, 0):
|
||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||
else:
|
||||
try:
|
||||
j = json.loads(request.data)
|
||||
log.info('Getting change details')
|
||||
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
|
||||
log.debug('%r', response)
|
||||
if response:
|
||||
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): # nosec
|
||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
|
||||
log.info('Database file updated')
|
||||
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
|
||||
log.info('Backing up existing and downloading updated metadata.db')
|
||||
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
|
||||
log.info('Setting up new DB')
|
||||
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
||||
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
return ''
|
||||
log.info('Database file updated')
|
||||
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
|
||||
log.info('Backing up existing and downloading updated metadata.db')
|
||||
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
|
||||
log.info('Setting up new DB')
|
||||
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
||||
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
return ''
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
+23
-2
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
@@ -34,6 +33,16 @@ try:
|
||||
except ImportError:
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||
from sqlalchemy.sql.expression import text
|
||||
|
||||
try:
|
||||
from six import __version__ as six_version
|
||||
except ImportError:
|
||||
six_version = "not installed"
|
||||
try:
|
||||
from httplib2 import __version__ as httplib2_version
|
||||
except ImportError:
|
||||
httplib2_version = "not installed"
|
||||
|
||||
try:
|
||||
from apiclient import errors
|
||||
@@ -47,11 +56,13 @@ try:
|
||||
from pydrive2.auth import GoogleAuth
|
||||
from pydrive2.drive import GoogleDrive
|
||||
from pydrive2.auth import RefreshError
|
||||
from pydrive2.files import ApiRequestError
|
||||
except ImportError as err:
|
||||
try:
|
||||
from pydrive.auth import GoogleAuth
|
||||
from pydrive.drive import GoogleDrive
|
||||
from pydrive.auth import RefreshError
|
||||
from pydrive.files import ApiRequestError
|
||||
except ImportError as err:
|
||||
importError = err
|
||||
gdrive_support = False
|
||||
@@ -168,7 +179,7 @@ class PermissionAdded(Base):
|
||||
def migrate():
|
||||
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
|
||||
PermissionAdded.__table__.create(bind = engine)
|
||||
for sql in session.execute("select sql from sqlite_master where type='table'"):
|
||||
for sql in session.execute(text("select sql from sqlite_master where type='table'")):
|
||||
if 'CREATE TABLE gdrive_ids' in sql[0]:
|
||||
currUniqueConstraint = 'UNIQUE (gdrive_id)'
|
||||
if currUniqueConstraint in sql[0]:
|
||||
@@ -313,6 +324,11 @@ def getFolderId(path, drive):
|
||||
log.error("gdrive.db DB is not Writeable")
|
||||
log.debug('Database error: %s', ex)
|
||||
session.rollback()
|
||||
except ApiRequestError as ex:
|
||||
log.error('{} {}'.format(ex.error['message'], path))
|
||||
session.rollback()
|
||||
except RefreshError as ex:
|
||||
log.error(ex)
|
||||
return currentFolderId
|
||||
|
||||
|
||||
@@ -659,3 +675,8 @@ def get_error_text(client_secrets=None):
|
||||
return 'Callback url (redirect url) is missing in client_secrets.json'
|
||||
if client_secrets:
|
||||
client_secrets.update(filedata['web'])
|
||||
|
||||
|
||||
def get_versions():
|
||||
return {'six': six_version,
|
||||
'httplib2': httplib2_version}
|
||||
|
||||
+58
-58
@@ -17,19 +17,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import io
|
||||
import mimetypes
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import socket
|
||||
import unicodedata
|
||||
from datetime import datetime, timedelta
|
||||
from tempfile import gettempdir
|
||||
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
from babel.dates import format_datetime
|
||||
from babel.units import format_unit
|
||||
from flask import send_from_directory, make_response, redirect, abort, url_for
|
||||
@@ -38,11 +37,8 @@ from flask_login import current_user
|
||||
from sqlalchemy.sql.expression import true, false, and_, text, func
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
try:
|
||||
from urllib.parse import quote
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
from markupsafe import escape
|
||||
from urllib.parse import quote
|
||||
|
||||
try:
|
||||
import unidecode
|
||||
@@ -50,7 +46,7 @@ try:
|
||||
except ImportError:
|
||||
use_unidecode = False
|
||||
|
||||
from . import calibre_db
|
||||
from . import calibre_db, cli
|
||||
from .tasks.convert import TaskConvert
|
||||
from . import logger, config, get_locale, db, ub
|
||||
from . import gdriveutils as gd
|
||||
@@ -63,7 +59,7 @@ log = logger.create()
|
||||
|
||||
try:
|
||||
from wand.image import Image
|
||||
from wand.exceptions import MissingDelegateError
|
||||
from wand.exceptions import MissingDelegateError, BlobError
|
||||
use_IM = True
|
||||
except (ImportError, RuntimeError) as e:
|
||||
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
|
||||
@@ -97,10 +93,11 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
||||
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
||||
else:
|
||||
settings = dict()
|
||||
txt = (u"%s -> %s: %s" % (
|
||||
old_book_format,
|
||||
new_book_format,
|
||||
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + book.title + "</a>"))
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss
|
||||
txt = u"{} -> {}: {}".format(
|
||||
old_book_format.upper(),
|
||||
new_book_format.upper(),
|
||||
link)
|
||||
settings['old_book_format'] = old_book_format
|
||||
settings['new_book_format'] = new_book_format
|
||||
WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, kindle_mail, user_id))
|
||||
@@ -214,9 +211,11 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
|
||||
for entry in iter(book.data):
|
||||
if entry.format.upper() == book_format.upper():
|
||||
converted_file_name = entry.name + '.' + book_format.lower()
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
|
||||
EmailText = _(u"%(book)s send to Kindle", book=link)
|
||||
WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
|
||||
config.get_mail_settings(), kindle_mail,
|
||||
_(u"E-mail: %(book)s", book=book.title), _(u'This e-mail has been sent via Calibre-Web.')))
|
||||
EmailText, _(u'This e-mail has been sent via Calibre-Web.')))
|
||||
return
|
||||
return _(u"The requested file could not be read. Maybe wrong permissions?")
|
||||
|
||||
@@ -230,16 +229,14 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||
value = value[:-1]+u'_'
|
||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||
if use_unidecode:
|
||||
value = (unidecode.unidecode(value))
|
||||
if config.config_unicode_filename:
|
||||
value = (unidecode.unidecode(value))
|
||||
else:
|
||||
value = value.replace(u'§', u'SS')
|
||||
value = value.replace(u'ß', u'ss')
|
||||
value = unicodedata.normalize('NFKD', value)
|
||||
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
|
||||
if isinstance(value, str): # Python3 str, Python2 unicode
|
||||
value = re_slugify.sub('', value)
|
||||
else:
|
||||
value = unicode(re_slugify.sub('', value))
|
||||
value = re_slugify.sub('', value)
|
||||
if replace_whitespace:
|
||||
# *+:\"/<>? are replaced by _
|
||||
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
||||
@@ -248,10 +245,7 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||
value = value[:128].strip()
|
||||
if not value:
|
||||
raise ValueError("Filename cannot be empty")
|
||||
if sys.version_info.major == 3:
|
||||
return value
|
||||
else:
|
||||
return value.decode('utf-8')
|
||||
return value
|
||||
|
||||
|
||||
def split_authors(values):
|
||||
@@ -329,11 +323,12 @@ def delete_book_file(book, calibrepath, book_format=None):
|
||||
except (IOError, OSError) as e:
|
||||
log.error("Deleting authorpath for book %s failed: %s", book.id, e)
|
||||
return True, None
|
||||
else:
|
||||
log.error("Deleting book %s failed, book path not valid: %s", book.id, book.path)
|
||||
return True, _("Deleting book %(id)s, book path not valid: %(path)s",
|
||||
id=book.id,
|
||||
path=book.path)
|
||||
|
||||
log.error("Deleting book %s from database only, book path in database not valid: %s",
|
||||
book.id, book.path)
|
||||
return True, _("Deleting book %(id)s from database only, book path in database not valid: %(path)s",
|
||||
id=book.id,
|
||||
path=book.path)
|
||||
|
||||
|
||||
# Moves files in file storage during author/title rename, or from temp dir to file storage
|
||||
@@ -382,7 +377,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author, orignal_filepa
|
||||
# os.unlink(os.path.normcase(os.path.join(dir_name, file)))
|
||||
# change location in database to new author/title path
|
||||
localbook.path = os.path.join(new_authordir, new_titledir).replace('\\','/')
|
||||
except OSError as ex:
|
||||
except (OSError) as ex:
|
||||
log.error("Rename title from: %s to %s: %s", path, new_path, ex)
|
||||
log.debug(ex, exc_info=True)
|
||||
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||
@@ -397,7 +392,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author, orignal_filepa
|
||||
file_format.name = new_name
|
||||
if not orignal_filepath and len(os.listdir(os.path.dirname(path))) == 0:
|
||||
shutil.rmtree(os.path.dirname(path))
|
||||
except OSError as ex:
|
||||
except (OSError) as ex:
|
||||
log.error("Rename file in path %s to %s: %s", new_path, new_name, ex)
|
||||
log.debug(ex, exc_info=True)
|
||||
return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||
@@ -490,10 +485,7 @@ def reset_password(user_id):
|
||||
def generate_random_password():
|
||||
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
||||
passlen = 8
|
||||
if sys.version_info < (3, 0):
|
||||
return "".join(s[ord(c) % len(s)] for c in os.urandom(passlen))
|
||||
else:
|
||||
return "".join(s[c % len(s)] for c in os.urandom(passlen))
|
||||
return "".join(s[c % len(s)] for c in os.urandom(passlen))
|
||||
|
||||
|
||||
def uniq(inpt):
|
||||
@@ -592,10 +584,17 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
||||
# saves book cover from url
|
||||
def save_cover_from_url(url, book_path):
|
||||
try:
|
||||
if not cli.allow_localhost:
|
||||
# 127.0.x.x, localhost, [::1], [::ffff:7f00:1]
|
||||
ip = socket.getaddrinfo(urlparse(url).hostname, 0)[0][4][0]
|
||||
if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1":
|
||||
log.error("Localhost was accessed for cover upload")
|
||||
return False, _("You are not allowed to access localhost for cover uploads")
|
||||
img = requests.get(url, timeout=(10, 200)) # ToDo: Error Handling
|
||||
img.raise_for_status()
|
||||
return save_cover(img, book_path)
|
||||
except (requests.exceptions.HTTPError,
|
||||
except (socket.gaierror,
|
||||
requests.exceptions.HTTPError,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout) as ex:
|
||||
log.info(u'Cover Download Error %s', ex)
|
||||
@@ -642,13 +641,17 @@ def save_cover(img, book_path):
|
||||
return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
|
||||
# convert to jpg because calibre only supports jpg
|
||||
if content_type != 'image/jpg':
|
||||
if hasattr(img, 'stream'):
|
||||
imgc = Image(blob=img.stream)
|
||||
else:
|
||||
imgc = Image(blob=io.BytesIO(img.content))
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace("rgb")
|
||||
img = imgc
|
||||
try:
|
||||
if hasattr(img, 'stream'):
|
||||
imgc = Image(blob=img.stream)
|
||||
else:
|
||||
imgc = Image(blob=io.BytesIO(img.content))
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace("rgb")
|
||||
img = imgc
|
||||
except (BlobError, MissingDelegateError):
|
||||
log.error("Invalid cover file content")
|
||||
return False, _("Invalid cover file content")
|
||||
else:
|
||||
if content_type not in 'image/jpeg':
|
||||
log.error("Only jpg/jpeg files are supported as coverfile")
|
||||
@@ -673,9 +676,9 @@ def save_cover(img, book_path):
|
||||
|
||||
def do_download_file(book, book_format, client, data, headers):
|
||||
if config.config_use_google_drive:
|
||||
startTime = time.time()
|
||||
#startTime = time.time()
|
||||
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
|
||||
log.debug('%s', time.time() - startTime)
|
||||
#log.debug('%s', time.time() - startTime)
|
||||
if df:
|
||||
return gd.do_gdrive_download(df, headers)
|
||||
else:
|
||||
@@ -707,15 +710,12 @@ def check_unrar(unrarLocation):
|
||||
return _('Unrar binary file not found')
|
||||
|
||||
try:
|
||||
if sys.version_info < (3, 0):
|
||||
unrarLocation = unrarLocation.encode(sys.getfilesystemencoding())
|
||||
unrarLocation = [unrarLocation]
|
||||
for lines in process_wait(unrarLocation):
|
||||
value = re.search('UNRAR (.*) freeware', lines, re.IGNORECASE)
|
||||
if value:
|
||||
version = value.group(1)
|
||||
log.debug("unrar version %s", version)
|
||||
break
|
||||
value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware')
|
||||
if value:
|
||||
version = value.group(1)
|
||||
log.debug("unrar version %s", version)
|
||||
|
||||
except (OSError, UnicodeDecodeError) as err:
|
||||
log.debug_or_exception(err)
|
||||
return _('Error excecuting UnRar')
|
||||
@@ -778,7 +778,7 @@ def render_task_status(tasklist):
|
||||
|
||||
ret['taskMessage'] = "{}: {}".format(_(task.name), task.message)
|
||||
ret['progress'] = "{} %".format(int(task.progress * 100))
|
||||
ret['user'] = user
|
||||
ret['user'] = escape(user) # prevent xss
|
||||
renderedtasklist.append(ret)
|
||||
|
||||
return renderedtasklist
|
||||
@@ -826,7 +826,7 @@ def get_cc_columns(filter_config_custom_read=False):
|
||||
|
||||
def get_download_link(book_id, book_format, client):
|
||||
book_format = book_format.split(".")[0]
|
||||
book = calibre_db.get_filtered_book(book_id)
|
||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||
if book:
|
||||
data1 = calibre_db.get_book_format(book.id, book_format.upper())
|
||||
else:
|
||||
@@ -838,8 +838,8 @@ def get_download_link(book_id, book_format, client):
|
||||
ub.update_download(book_id, int(current_user.id))
|
||||
file_name = book.title
|
||||
if len(book.authors) > 0:
|
||||
file_name = book.authors[0].name + '_' + file_name
|
||||
file_name = get_valid_filename(file_name)
|
||||
file_name = file_name + ' - ' + book.authors[0].name
|
||||
file_name = get_valid_filename(file_name, replace_whitespace=False)
|
||||
headers = Headers()
|
||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
||||
|
||||
+8
-3
@@ -16,9 +16,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
|
||||
from . import logger
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
try:
|
||||
@@ -52,7 +53,11 @@ def get_language_names(locale):
|
||||
|
||||
|
||||
def get_language_name(locale, lang_code):
|
||||
return get_language_names(locale)[lang_code]
|
||||
try:
|
||||
return get_language_names(locale)[lang_code]
|
||||
except KeyError:
|
||||
log.error('Missing translation for language name: {}'.format(lang_code))
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def get_language_codes(locale, language_names, remainder=None):
|
||||
|
||||
+854
-2
File diff suppressed because it is too large
Load Diff
+7
-6
@@ -22,7 +22,6 @@
|
||||
|
||||
# custom jinja filters
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import datetime
|
||||
import mimetypes
|
||||
from uuid import uuid4
|
||||
@@ -31,7 +30,7 @@ from babel.dates import format_date
|
||||
from flask import Blueprint, request, url_for
|
||||
from flask_babel import get_locale
|
||||
from flask_login import current_user
|
||||
|
||||
from markupsafe import escape
|
||||
from . import logger
|
||||
|
||||
|
||||
@@ -113,10 +112,8 @@ def yesno(value, yes, no):
|
||||
|
||||
@jinjia.app_template_filter('formatfloat')
|
||||
def formatfloat(value, decimals=1):
|
||||
formatedstring = '%d' % value
|
||||
if (value % 1) != 0:
|
||||
formatedstring = ('%s.%d' % (formatedstring, (value % 1) * 10**decimals)).rstrip('0')
|
||||
return formatedstring
|
||||
value = 0 if not value else value
|
||||
return ('{0:.' + str(decimals) + 'f}').format(value).rstrip('0').rstrip('.')
|
||||
|
||||
|
||||
@jinjia.app_template_filter('formatseriesindex')
|
||||
@@ -131,6 +128,10 @@ def formatseriesindex_filter(series_index):
|
||||
return series_index
|
||||
return 0
|
||||
|
||||
@jinjia.app_template_filter('escapedlink')
|
||||
def escapedlink_filter(url, text):
|
||||
return "<a href='{}'>{}</a>".format(url, escape(text))
|
||||
|
||||
@jinjia.app_template_filter('uuidfilter')
|
||||
def uuidfilter(var):
|
||||
return uuid4()
|
||||
|
||||
+231
-96
@@ -19,15 +19,11 @@
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import sys
|
||||
import os
|
||||
import uuid
|
||||
from time import gmtime, strftime
|
||||
|
||||
try:
|
||||
from urllib import unquote
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
@@ -42,11 +38,14 @@ from flask import (
|
||||
from flask_login import current_user
|
||||
from werkzeug.datastructures import Headers
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.sql.expression import and_
|
||||
from sqlalchemy.sql.expression import and_, or_
|
||||
from sqlalchemy.exc import StatementError
|
||||
from sqlalchemy.sql import select
|
||||
import requests
|
||||
|
||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub
|
||||
|
||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||
from .constants import sqlalchemy_version2
|
||||
from .helper import get_download_link
|
||||
from .services import SyncToken as SyncToken
|
||||
from .web import download_required
|
||||
@@ -81,6 +80,7 @@ CONNECTION_SPECIFIC_HEADERS = [
|
||||
"transfer-encoding",
|
||||
]
|
||||
|
||||
|
||||
def get_kobo_activated():
|
||||
return config.config_kobo_sync
|
||||
|
||||
@@ -99,6 +99,8 @@ def make_request_to_kobo_store(sync_token=None):
|
||||
allow_redirects=False,
|
||||
timeout=(2, 10)
|
||||
)
|
||||
log.debug("Content: " + str(store_response.content))
|
||||
log.debug("StatusCode: " + str(store_response.status_code))
|
||||
return store_response
|
||||
|
||||
|
||||
@@ -107,7 +109,8 @@ def redirect_or_proxy_request():
|
||||
if request.method == "GET":
|
||||
return redirect(get_store_url_for_current_request(), 307)
|
||||
else:
|
||||
# The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves.
|
||||
# The Kobo device turns other request types into GET requests on redirects,
|
||||
# so we instead proxy to the Kobo store ourselves.
|
||||
store_response = make_request_to_kobo_store()
|
||||
|
||||
response_headers = store_response.headers
|
||||
@@ -135,15 +138,20 @@ def convert_to_kobo_timestamp_string(timestamp):
|
||||
def HandleSyncRequest():
|
||||
sync_token = SyncToken.SyncToken.from_headers(request.headers)
|
||||
log.info("Kobo library sync request received.")
|
||||
log.debug("SyncToken: {}".format(sync_token))
|
||||
if not current_app.wsgi_app.is_proxied:
|
||||
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
||||
|
||||
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header
|
||||
# instead so that the device triggers another sync.
|
||||
# if no books synced don't respect sync_token
|
||||
if not ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == current_user.id).count():
|
||||
sync_token.books_last_modified = datetime.datetime.min
|
||||
sync_token.books_last_created = datetime.datetime.min
|
||||
sync_token.reading_state_last_modified = datetime.datetime.min
|
||||
|
||||
new_books_last_modified = sync_token.books_last_modified
|
||||
new_books_last_created = sync_token.books_last_created
|
||||
new_books_last_modified = sync_token.books_last_modified # needed for sync selected shelfs only
|
||||
new_books_last_created = sync_token.books_last_created # needed to distinguish between new and changed entitlement
|
||||
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
||||
|
||||
new_archived_last_modified = datetime.datetime.min
|
||||
sync_results = []
|
||||
|
||||
@@ -151,32 +159,61 @@ def HandleSyncRequest():
|
||||
# in case of external changes (e.g: adding a book through Calibre).
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
|
||||
if sync_token.books_last_id > -1:
|
||||
changed_entries = (
|
||||
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.filter(db.Books.last_modified >= sync_token.books_last_modified)
|
||||
.filter(db.Books.id>sync_token.books_last_id)
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.filter(calibre_db.common_filters())
|
||||
.order_by(db.Books.last_modified)
|
||||
.order_by(db.Books.id)
|
||||
.limit(SYNC_ITEM_LIMIT)
|
||||
only_kobo_shelves = current_user.kobo_only_shelves_sync
|
||||
|
||||
if only_kobo_shelves:
|
||||
if sqlalchemy_version2:
|
||||
changed_entries = select(db.Books,
|
||||
ub.ArchivedBook.last_modified,
|
||||
ub.BookShelf.date_added,
|
||||
ub.ArchivedBook.is_archived)
|
||||
else:
|
||||
changed_entries = calibre_db.session.query(db.Books,
|
||||
ub.ArchivedBook.last_modified,
|
||||
ub.BookShelf.date_added,
|
||||
ub.ArchivedBook.is_archived)
|
||||
changed_entries = (changed_entries
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
||||
ub.ArchivedBook.user_id == current_user.id))
|
||||
.filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
|
||||
.filter(ub.KoboSyncedBooks.user_id == current_user.id)))
|
||||
.filter(ub.BookShelf.date_added > sync_token.books_last_modified)
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.filter(calibre_db.common_filters(allow_show_archived=True))
|
||||
.order_by(db.Books.id)
|
||||
.order_by(ub.ArchivedBook.last_modified)
|
||||
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
|
||||
.join(ub.Shelf)
|
||||
.filter(ub.Shelf.user_id == current_user.id)
|
||||
.filter(ub.Shelf.kobo_sync)
|
||||
.distinct()
|
||||
)
|
||||
else:
|
||||
changed_entries = (
|
||||
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.filter(db.Books.last_modified > sync_token.books_last_modified)
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.filter(calibre_db.common_filters())
|
||||
.order_by(db.Books.last_modified)
|
||||
.order_by(db.Books.id)
|
||||
.limit(SYNC_ITEM_LIMIT)
|
||||
if sqlalchemy_version2:
|
||||
changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||
else:
|
||||
changed_entries = calibre_db.session.query(db.Books,
|
||||
ub.ArchivedBook.last_modified,
|
||||
ub.ArchivedBook.is_archived)
|
||||
changed_entries = (changed_entries
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
||||
ub.ArchivedBook.user_id == current_user.id))
|
||||
.filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
|
||||
.filter(ub.KoboSyncedBooks.user_id == current_user.id)))
|
||||
.filter(calibre_db.common_filters(allow_show_archived=True))
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.order_by(db.Books.last_modified)
|
||||
.order_by(db.Books.id)
|
||||
)
|
||||
|
||||
|
||||
reading_states_in_new_entitlements = []
|
||||
for book in changed_entries:
|
||||
if sqlalchemy_version2:
|
||||
books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
|
||||
else:
|
||||
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
||||
log.debug("Books to Sync: {}".format(len(books.all())))
|
||||
for book in books:
|
||||
formats = [data.format for data in book.Books.data]
|
||||
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||
@@ -192,7 +229,14 @@ def HandleSyncRequest():
|
||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||
reading_states_in_new_entitlements.append(book.Books.id)
|
||||
|
||||
if book.Books.timestamp > sync_token.books_last_created:
|
||||
ts_created = book.Books.timestamp
|
||||
|
||||
try:
|
||||
ts_created = max(ts_created, book.date_added)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if ts_created > sync_token.books_last_created:
|
||||
sync_results.append({"NewEntitlement": entitlement})
|
||||
else:
|
||||
sync_results.append({"ChangedEntitlement": entitlement})
|
||||
@@ -200,36 +244,63 @@ def HandleSyncRequest():
|
||||
new_books_last_modified = max(
|
||||
book.Books.last_modified, new_books_last_modified
|
||||
)
|
||||
new_books_last_created = max(book.Books.timestamp, new_books_last_created)
|
||||
try:
|
||||
new_books_last_modified = max(
|
||||
new_books_last_modified, book.date_added
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
max_change = (changed_entries
|
||||
.from_self()
|
||||
.filter(ub.ArchivedBook.is_archived)
|
||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc())
|
||||
.first()
|
||||
)
|
||||
if max_change:
|
||||
max_change = max_change.last_modified
|
||||
new_books_last_created = max(ts_created, new_books_last_created)
|
||||
kobo_sync_status.add_synced_books(book.Books.id)
|
||||
|
||||
if sqlalchemy_version2:
|
||||
max_change = calibre_db.session.execute(changed_entries
|
||||
.filter(ub.ArchivedBook.is_archived)
|
||||
.filter(ub.ArchivedBook.user_id == current_user.id)
|
||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
|
||||
.columns(db.Books).first()
|
||||
else:
|
||||
max_change = new_archived_last_modified
|
||||
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
|
||||
.filter(ub.ArchivedBook.user_id==current_user.id) \
|
||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
|
||||
|
||||
max_change = max_change.last_modified if max_change else new_archived_last_modified
|
||||
|
||||
new_archived_last_modified = max(new_archived_last_modified, max_change)
|
||||
|
||||
# no. of books returned
|
||||
book_count = changed_entries.count()
|
||||
|
||||
# last entry:
|
||||
if book_count:
|
||||
books_last_id = changed_entries.all()[-1].Books.id or -1
|
||||
if sqlalchemy_version2:
|
||||
entries = calibre_db.session.execute(changed_entries).all()
|
||||
book_count = len(entries)
|
||||
else:
|
||||
books_last_id = -1
|
||||
|
||||
book_count = changed_entries.count()
|
||||
# last entry:
|
||||
cont_sync = bool(book_count)
|
||||
log.debug("Remaining books to Sync: {}".format(book_count))
|
||||
# generate reading state data
|
||||
changed_reading_states = (
|
||||
ub.session.query(ub.KoboReadingState)
|
||||
.filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
|
||||
ub.KoboReadingState.user_id == current_user.id,
|
||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))))
|
||||
for kobo_reading_state in changed_reading_states.all():
|
||||
changed_reading_states = ub.session.query(ub.KoboReadingState)
|
||||
|
||||
if only_kobo_shelves:
|
||||
changed_reading_states = changed_reading_states.join(ub.BookShelf,
|
||||
ub.KoboReadingState.book_id == ub.BookShelf.book_id)\
|
||||
.join(ub.Shelf)\
|
||||
.filter(current_user.id == ub.Shelf.user_id)\
|
||||
.filter(ub.Shelf.kobo_sync,
|
||||
or_(
|
||||
ub.KoboReadingState.last_modified > sync_token.reading_state_last_modified,
|
||||
func.datetime(ub.BookShelf.date_added) > sync_token.books_last_modified
|
||||
)).distinct()
|
||||
else:
|
||||
changed_reading_states = changed_reading_states.filter(
|
||||
ub.KoboReadingState.last_modified > sync_token.reading_state_last_modified)
|
||||
|
||||
changed_reading_states = changed_reading_states.filter(
|
||||
and_(ub.KoboReadingState.user_id == current_user.id,
|
||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))\
|
||||
.order_by(ub.KoboReadingState.last_modified)
|
||||
cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT)
|
||||
for kobo_reading_state in changed_reading_states.limit(SYNC_ITEM_LIMIT).all():
|
||||
book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
|
||||
if book:
|
||||
sync_results.append({
|
||||
@@ -239,20 +310,21 @@ def HandleSyncRequest():
|
||||
})
|
||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||
|
||||
sync_shelves(sync_token, sync_results)
|
||||
sync_shelves(sync_token, sync_results, only_kobo_shelves)
|
||||
|
||||
sync_token.books_last_created = new_books_last_created
|
||||
# update last created timestamp to distinguish between new and changed entitlements
|
||||
if not cont_sync:
|
||||
sync_token.books_last_created = new_books_last_created
|
||||
sync_token.books_last_modified = new_books_last_modified
|
||||
sync_token.archive_last_modified = new_archived_last_modified
|
||||
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
||||
sync_token.books_last_id = books_last_id
|
||||
|
||||
return generate_sync_response(sync_token, sync_results, book_count)
|
||||
return generate_sync_response(sync_token, sync_results, cont_sync)
|
||||
|
||||
|
||||
def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||
extra_headers = {}
|
||||
if config.config_kobo_proxy:
|
||||
if config.config_kobo_proxy and not set_cont:
|
||||
# Merge in sync results from the official Kobo store.
|
||||
try:
|
||||
store_response = make_request_to_kobo_store(sync_token)
|
||||
@@ -270,8 +342,10 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||
extra_headers["x-kobo-sync"] = "continue"
|
||||
sync_token.to_headers(extra_headers)
|
||||
|
||||
response = make_response(jsonify(sync_results), extra_headers)
|
||||
|
||||
# log.debug("Kobo Sync Content: {}".format(sync_results))
|
||||
# jsonify decodes the unicode string different to what kobo expects
|
||||
response = make_response(json.dumps(sync_results), extra_headers)
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
|
||||
@@ -288,7 +362,9 @@ def HandleMetadataRequest(book_uuid):
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
metadata = get_metadata(book)
|
||||
return jsonify([metadata])
|
||||
response = make_response(json.dumps([metadata], ensure_ascii=False))
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
|
||||
def get_download_url_for_book(book, book_format):
|
||||
@@ -316,7 +392,7 @@ def get_download_url_for_book(book, book_format):
|
||||
|
||||
|
||||
def create_book_entitlement(book, archived):
|
||||
book_uuid = book.uuid
|
||||
book_uuid = str(book.uuid)
|
||||
return {
|
||||
"Accessibility": "Full",
|
||||
"ActivePeriod": {"From": convert_to_kobo_timestamp_string(datetime.datetime.now())},
|
||||
@@ -343,18 +419,15 @@ def get_description(book):
|
||||
return book.comments[0].text
|
||||
|
||||
|
||||
# TODO handle multiple authors
|
||||
def get_author(book):
|
||||
if not book.authors:
|
||||
return {"Contributors": None}
|
||||
if len(book.authors) > 1:
|
||||
author_list = []
|
||||
autor_roles = []
|
||||
for author in book.authors:
|
||||
autor_roles.append({"Name":author.name, "Role":"Author"})
|
||||
author_list.append(author.name)
|
||||
return {"ContributorRoles": autor_roles, "Contributors":author_list}
|
||||
return {"ContributorRoles": [{"Name":book.authors[0].name, "Role":"Author"}], "Contributors": book.authors[0].name}
|
||||
author_list = []
|
||||
autor_roles = []
|
||||
for author in book.authors:
|
||||
autor_roles.append({"Name":author.name}) #.encode('unicode-escape').decode('latin-1')
|
||||
author_list.append(author.name)
|
||||
return {"ContributorRoles": autor_roles, "Contributors":author_list}
|
||||
|
||||
|
||||
def get_publisher(book):
|
||||
@@ -394,7 +467,7 @@ def get_metadata(book):
|
||||
|
||||
book_uuid = book.uuid
|
||||
metadata = {
|
||||
"Categories": ["00000000-0000-0000-0000-000000000001",],
|
||||
"Categories": ["00000000-0000-0000-0000-000000000001", ],
|
||||
# "Contributors": get_author(book),
|
||||
"CoverImageId": book_uuid,
|
||||
"CrossRevisionId": book_uuid,
|
||||
@@ -411,9 +484,7 @@ def get_metadata(book):
|
||||
"IsSocialEnabled": True,
|
||||
"Language": "en",
|
||||
"PhoneticPronunciations": {},
|
||||
# TODO: Fix book.pubdate to return a datetime object so that we can easily
|
||||
# convert it to the format Kobo devices expect.
|
||||
"PublicationDate": book.pubdate,
|
||||
"PublicationDate": convert_to_kobo_timestamp_string(book.pubdate),
|
||||
"Publisher": {"Imprint": "", "Name": get_publisher(book),},
|
||||
"RevisionId": book_uuid,
|
||||
"Title": book.title,
|
||||
@@ -422,21 +493,18 @@ def get_metadata(book):
|
||||
metadata.update(get_author(book))
|
||||
|
||||
if get_series(book):
|
||||
if sys.version_info < (3, 0):
|
||||
name = get_series(book).encode("utf-8")
|
||||
else:
|
||||
name = get_series(book)
|
||||
name = get_series(book)
|
||||
metadata["Series"] = {
|
||||
"Name": get_series(book),
|
||||
"Number": get_seriesindex(book), # ToDo Check int() ?
|
||||
"NumberFloat": float(get_seriesindex(book)),
|
||||
# Get a deterministic id based on the series name.
|
||||
"Id": uuid.uuid3(uuid.NAMESPACE_DNS, name),
|
||||
"Id": str(uuid.uuid3(uuid.NAMESPACE_DNS, name)),
|
||||
}
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||
@requires_kobo_auth
|
||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||
@@ -471,6 +539,7 @@ def HandleTagCreate():
|
||||
return make_response(jsonify(str(shelf.uuid)), 201)
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
||||
@requires_kobo_auth
|
||||
def HandleTagUpdate(tag_id):
|
||||
@@ -526,6 +595,7 @@ def add_items_to_shelf(items, shelf):
|
||||
return items_unknown_to_calibre
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
def HandleTagAddItem(tag_id):
|
||||
@@ -555,6 +625,7 @@ def HandleTagAddItem(tag_id):
|
||||
return make_response('', 201)
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
def HandleTagRemoveItem(tag_id):
|
||||
@@ -601,13 +672,11 @@ def HandleTagRemoveItem(tag_id):
|
||||
|
||||
# Add new, changed, or deleted shelves to the sync_results.
|
||||
# Note: Public shelves that aren't owned by the user aren't supported.
|
||||
def sync_shelves(sync_token, sync_results):
|
||||
def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
||||
new_tags_last_modified = sync_token.tags_last_modified
|
||||
|
||||
for shelf in ub.session.query(ub.ShelfArchive).filter(func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified,
|
||||
ub.ShelfArchive.user_id == current_user.id):
|
||||
# transmit all archived shelfs independent of last sync (why should this matter?)
|
||||
for shelf in ub.session.query(ub.ShelfArchive).filter(ub.ShelfArchive.user_id == current_user.id):
|
||||
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
|
||||
|
||||
sync_results.append({
|
||||
"DeletedTag": {
|
||||
"Tag": {
|
||||
@@ -616,9 +685,43 @@ def sync_shelves(sync_token, sync_results):
|
||||
}
|
||||
}
|
||||
})
|
||||
ub.session.delete(shelf)
|
||||
ub.session_commit()
|
||||
|
||||
for shelf in ub.session.query(ub.Shelf).filter(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||
ub.Shelf.user_id == current_user.id):
|
||||
extra_filters = []
|
||||
if only_kobo_shelves:
|
||||
for shelf in ub.session.query(ub.Shelf).filter(
|
||||
func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||
ub.Shelf.user_id == current_user.id,
|
||||
not ub.Shelf.kobo_sync
|
||||
):
|
||||
sync_results.append({
|
||||
"DeletedTag": {
|
||||
"Tag": {
|
||||
"Id": shelf.uuid,
|
||||
"LastModified": convert_to_kobo_timestamp_string(shelf.last_modified)
|
||||
}
|
||||
}
|
||||
})
|
||||
extra_filters.append(ub.Shelf.kobo_sync)
|
||||
|
||||
if sqlalchemy_version2:
|
||||
shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
||||
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
||||
ub.Shelf.user_id == current_user.id,
|
||||
*extra_filters
|
||||
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf)
|
||||
else:
|
||||
shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
||||
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
||||
ub.Shelf.user_id == current_user.id,
|
||||
*extra_filters
|
||||
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())
|
||||
|
||||
|
||||
for shelf in shelflist:
|
||||
if not shelf_lib.check_shelf_view_permissions(shelf):
|
||||
continue
|
||||
|
||||
@@ -663,7 +766,7 @@ def create_kobo_tag(shelf):
|
||||
)
|
||||
return {"Tag": tag}
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
||||
@requires_kobo_auth
|
||||
def HandleStateRequest(book_uuid):
|
||||
@@ -747,7 +850,7 @@ def get_ub_read_status(kobo_read_status):
|
||||
|
||||
def get_or_create_reading_state(book_id):
|
||||
book_read = ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id,
|
||||
ub.ReadBook.user_id == current_user.id).one_or_none()
|
||||
ub.ReadBook.user_id == int(current_user.id)).one_or_none()
|
||||
if not book_read:
|
||||
book_read = ub.ReadBook(user_id=current_user.id, book_id=book_id)
|
||||
if not book_read.kobo_reading_state:
|
||||
@@ -838,6 +941,7 @@ def TopLevelEndpoint():
|
||||
return make_response(jsonify({}))
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
||||
@requires_kobo_auth
|
||||
def HandleBookDeletionRequest(book_uuid):
|
||||
@@ -860,10 +964,13 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
|
||||
ub.session.merge(archived_book)
|
||||
ub.session_commit()
|
||||
return ("", 204)
|
||||
if archived_book.is_archived:
|
||||
kobo_sync_status.remove_synced_book(book_id)
|
||||
return "", 204
|
||||
|
||||
|
||||
# TODO: Implement the following routes
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||
def HandleUnimplementedRequest(dummy=None):
|
||||
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
||||
@@ -871,6 +978,7 @@ def HandleUnimplementedRequest(dummy=None):
|
||||
|
||||
|
||||
# TODO: Implement the following routes
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
||||
@@ -881,15 +989,41 @@ def HandleUserRequest(dummy=None):
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
|
||||
def handle_benefits():
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
return make_response(jsonify({"Benefits": {}}))
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
|
||||
def handle_getests():
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
testkey = request.headers.get("X-Kobo-userkey","")
|
||||
return make_response(jsonify({"Result": "Success", "TestKey":testkey, "Tests": {}}))
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/featured/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/featured/", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/external/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/series/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/<dummy>/", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/deals", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
||||
def HandleProductsRequest(dummy=None):
|
||||
log.debug("Unimplemented Products Request received: %s", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
@@ -914,6 +1048,7 @@ def make_calibre_web_auth_response():
|
||||
)
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
def HandleAuthRequest():
|
||||
|
||||
+30
-40
@@ -62,6 +62,7 @@ particular calls to non-Kobo specific endpoints such as the CalibreWeb book down
|
||||
from binascii import hexlify
|
||||
from datetime import datetime
|
||||
from os import urandom
|
||||
from functools import wraps
|
||||
|
||||
from flask import g, Blueprint, url_for, abort, request
|
||||
from flask_login import login_user, current_user, login_required
|
||||
@@ -70,11 +71,6 @@ from flask_babel import gettext as _
|
||||
from . import logger, config, calibre_db, db, helper, ub, lm
|
||||
from .render_template import render_title_template
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError:
|
||||
pass # We're not using Python 3
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
@@ -122,55 +118,49 @@ 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):
|
||||
warning = False
|
||||
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 host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f') or host == "[::1]":
|
||||
warning = _('Please access Calibre-Web from non localhost to get valid api_endpoint for kobo device')
|
||||
|
||||
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
|
||||
# Generate auth token if none is existing for this user
|
||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
|
||||
ub.RemoteAuthToken.user_id == user_id
|
||||
).filter(ub.RemoteAuthToken.token_type==1).first()
|
||||
|
||||
ub.session.add(auth_token)
|
||||
ub.session_commit()
|
||||
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
|
||||
|
||||
books = calibre_db.session.query(db.Books).join(db.Data).all()
|
||||
ub.session.add(auth_token)
|
||||
ub.session_commit()
|
||||
|
||||
for book in books:
|
||||
formats = [data.format for data in book.data]
|
||||
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||
helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||
books = calibre_db.session.query(db.Books).join(db.Data).all()
|
||||
|
||||
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
|
||||
)
|
||||
for book in books:
|
||||
formats = [data.format for data in book.data]
|
||||
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||
helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||
|
||||
return render_title_template(
|
||||
"generate_kobo_auth_url.html",
|
||||
title=_(u"Kobo Setup"),
|
||||
auth_token=auth_token.auth_token,
|
||||
warning = warning
|
||||
)
|
||||
|
||||
|
||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>")
|
||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_auth_token(user_id):
|
||||
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||
# Invalidate any previously 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()
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2021 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask_login import current_user
|
||||
from . import ub
|
||||
import datetime
|
||||
from sqlalchemy.sql.expression import or_, and_, true
|
||||
from sqlalchemy import exc
|
||||
|
||||
# Add the current book id to kobo_synced_books table for current user, if entry is already present,
|
||||
# do nothing (safety precaution)
|
||||
def add_synced_books(book_id):
|
||||
is_present = ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id)\
|
||||
.filter(ub.KoboSyncedBooks.user_id == current_user.id).count()
|
||||
if not is_present:
|
||||
synced_book = ub.KoboSyncedBooks()
|
||||
synced_book.user_id = current_user.id
|
||||
synced_book.book_id = book_id
|
||||
ub.session.add(synced_book)
|
||||
ub.session_commit()
|
||||
|
||||
|
||||
# Select all entries of current book in kobo_synced_books table, which are from current user and delete them
|
||||
def remove_synced_book(book_id, all=False, session=None):
|
||||
if not all:
|
||||
user = ub.KoboSyncedBooks.user_id == current_user.id
|
||||
else:
|
||||
user = true()
|
||||
if not session:
|
||||
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete()
|
||||
ub.session_commit()
|
||||
else:
|
||||
session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete()
|
||||
ub.session_commit(sess=session)
|
||||
|
||||
|
||||
|
||||
def change_archived_books(book_id, state=None, message=None):
|
||||
archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id),
|
||||
ub.ArchivedBook.book_id == book_id)).first()
|
||||
if not archived_book:
|
||||
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
||||
|
||||
archived_book.is_archived = state if state else not archived_book.is_archived
|
||||
archived_book.last_modified = datetime.datetime.utcnow()
|
||||
|
||||
ub.session.merge(archived_book)
|
||||
ub.session_commit(message)
|
||||
return archived_book.is_archived
|
||||
|
||||
|
||||
# select all books which are synced by the current user and do not belong to a synced shelf and set them to archive
|
||||
# select all shelves from current user which are synced and do not belong to the "only sync" shelves
|
||||
def update_on_sync_shelfs(user_id):
|
||||
books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
|
||||
.join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True)
|
||||
.join(ub.Shelf, ub.Shelf.user_id == user_id, isouter=True)
|
||||
.filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None))
|
||||
.filter(ub.KoboSyncedBooks.user_id == user_id).all())
|
||||
for b in books_to_archive:
|
||||
change_archived_books(b.book_id, True)
|
||||
ub.session.query(ub.KoboSyncedBooks) \
|
||||
.filter(ub.KoboSyncedBooks.book_id == b.book_id) \
|
||||
.filter(ub.KoboSyncedBooks.user_id == user_id).delete()
|
||||
ub.session_commit()
|
||||
|
||||
# Search all shelf which are currently not synced
|
||||
shelves_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == user_id).filter(
|
||||
ub.Shelf.kobo_sync == 0).all()
|
||||
for a in shelves_to_archive:
|
||||
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=user_id))
|
||||
ub.session_commit()
|
||||
+4
-10
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import sys
|
||||
import inspect
|
||||
@@ -43,20 +42,15 @@ logging.addLevelName(logging.CRITICAL, "CRIT")
|
||||
|
||||
class _Logger(logging.Logger):
|
||||
|
||||
def debug_or_exception(self, message, *args, **kwargs):
|
||||
def debug_or_exception(self, message, stacklevel=2, *args, **kwargs):
|
||||
if sys.version_info > (3, 7):
|
||||
if is_debug_enabled():
|
||||
self.exception(message, stacklevel=2, *args, **kwargs)
|
||||
self.exception(message, stacklevel=stacklevel, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, stacklevel=2, *args, **kwargs)
|
||||
elif sys.version_info > (3, 0):
|
||||
if is_debug_enabled():
|
||||
self.exception(message, stack_info=True, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, *args, **kwargs)
|
||||
self.error(message, stacklevel=stacklevel, *args, **kwargs)
|
||||
else:
|
||||
if is_debug_enabled():
|
||||
self.exception(message, *args, **kwargs)
|
||||
self.exception(message, stack_info=True, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2021 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ComicVine api document: https://comicvine.gamespot.com/api/documentation
|
||||
|
||||
import requests
|
||||
from cps.services.Metadata import Metadata
|
||||
|
||||
|
||||
class ComicVine(Metadata):
|
||||
__name__ = "ComicVine"
|
||||
__id__ = "comicvine"
|
||||
|
||||
def search(self, query, generic_cover=""):
|
||||
val = list()
|
||||
apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6"
|
||||
if self.active:
|
||||
headers = {
|
||||
'User-Agent': 'Not Evil Browser'
|
||||
}
|
||||
|
||||
result = requests.get("https://comicvine.gamespot.com/api/search?api_key="
|
||||
+ apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers)
|
||||
for r in result.json().get('results'):
|
||||
seriesTitle = r['volume'].get('name', "")
|
||||
if r.get('store_date'):
|
||||
dateFomers = r.get('store_date')
|
||||
else:
|
||||
dateFomers = r.get('date_added')
|
||||
v = dict()
|
||||
v['id'] = r['id']
|
||||
v['title'] = seriesTitle + " #" + r.get('issue_number', "0") + " - " + ( r.get('name', "") or "")
|
||||
v['authors'] = r.get('authors', [])
|
||||
v['description'] = r.get('description', "")
|
||||
v['publisher'] = ""
|
||||
v['publishedDate'] = dateFomers
|
||||
v['tags'] = ["Comics", seriesTitle]
|
||||
v['rating'] = 0
|
||||
v['series'] = seriesTitle
|
||||
v['cover'] = r['image'].get('original_url')
|
||||
v['source'] = {
|
||||
"id": self.__id__,
|
||||
"description": "ComicVine Books",
|
||||
"link": "https://comicvine.gamespot.com/"
|
||||
}
|
||||
v['url'] = r.get('site_detail_url', "")
|
||||
val.append(v)
|
||||
return val
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2021 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Google Books api document: https://developers.google.com/books/docs/v1/using
|
||||
|
||||
|
||||
import requests
|
||||
from cps.services.Metadata import Metadata
|
||||
|
||||
class Google(Metadata):
|
||||
__name__ = "Google"
|
||||
__id__ = "google"
|
||||
|
||||
def search(self, query, generic_cover=""):
|
||||
if self.active:
|
||||
val = list()
|
||||
result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+"))
|
||||
for r in result.json().get('items'):
|
||||
v = dict()
|
||||
v['id'] = r['id']
|
||||
v['title'] = r['volumeInfo'].get('title',"")
|
||||
v['authors'] = r['volumeInfo'].get('authors', [])
|
||||
v['description'] = r['volumeInfo'].get('description', "")
|
||||
v['publisher'] = r['volumeInfo'].get('publisher', "")
|
||||
v['publishedDate'] = r['volumeInfo'].get('publishedDate', "")
|
||||
v['tags'] = r['volumeInfo'].get('categories', [])
|
||||
v['rating'] = r['volumeInfo'].get('averageRating', 0)
|
||||
if r['volumeInfo'].get('imageLinks'):
|
||||
v['cover'] = r['volumeInfo']['imageLinks']['thumbnail'].replace("http://", "https://")
|
||||
else:
|
||||
v['cover'] = "/../../../static/generic_cover.jpg"
|
||||
v['source'] = {
|
||||
"id": self.__id__,
|
||||
"description": "Google Books",
|
||||
"link": "https://books.google.com/"}
|
||||
v['url'] = "https://books.google.com/books?id=" + r['id']
|
||||
val.append(v)
|
||||
return val
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2021 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from scholarly import scholarly
|
||||
|
||||
from cps.services.Metadata import Metadata
|
||||
|
||||
class scholar(Metadata):
|
||||
__name__ = "Google Scholar"
|
||||
__id__ = "googlescholar"
|
||||
|
||||
def search(self, query, generic_cover=""):
|
||||
val = list()
|
||||
if self.active:
|
||||
scholar_gen = scholarly.search_pubs(' '.join(query.split('+')))
|
||||
i = 0
|
||||
for publication in scholar_gen:
|
||||
v = dict()
|
||||
v['id'] = publication['url_scholarbib'].split(':')[1]
|
||||
v['title'] = publication['bib'].get('title')
|
||||
v['authors'] = publication['bib'].get('author', [])
|
||||
v['description'] = publication['bib'].get('abstract', "")
|
||||
v['publisher'] = publication['bib'].get('venue', "")
|
||||
if publication['bib'].get('pub_year'):
|
||||
v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01"
|
||||
else:
|
||||
v['publishedDate'] = ""
|
||||
v['tags'] = []
|
||||
v['rating'] = 0
|
||||
v['series'] = ""
|
||||
v['cover'] = ""
|
||||
v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "",
|
||||
v['source'] = {
|
||||
"id": self.__id__,
|
||||
"description": "Google Scholar",
|
||||
"link": "https://scholar.google.com/"
|
||||
}
|
||||
val.append(v)
|
||||
i += 1
|
||||
if (i >= 10):
|
||||
break
|
||||
return val
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
from flask import session
|
||||
|
||||
try:
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import json
|
||||
from functools import wraps
|
||||
|
||||
|
||||
+13
-17
@@ -20,8 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import datetime
|
||||
from functools import wraps
|
||||
|
||||
@@ -36,8 +34,6 @@ from .pagination import Pagination
|
||||
from .web import render_read_books
|
||||
from .usermanagement import load_user_from_request
|
||||
from flask_babel import gettext as _
|
||||
from babel import Locale as LC
|
||||
from babel.core import UnknownLocaleError
|
||||
|
||||
opds = Blueprint('opds', __name__)
|
||||
|
||||
@@ -433,16 +429,17 @@ def feed_languagesindex():
|
||||
if current_user.filter_language() == u"all":
|
||||
languages = calibre_db.speaking_language()
|
||||
else:
|
||||
try:
|
||||
cur_l = LC.parse(current_user.filter_language())
|
||||
except UnknownLocaleError:
|
||||
cur_l = None
|
||||
#try:
|
||||
# cur_l = LC.parse(current_user.filter_language())
|
||||
#except UnknownLocaleError:
|
||||
# cur_l = None
|
||||
languages = calibre_db.session.query(db.Languages).filter(
|
||||
db.Languages.lang_code == current_user.filter_language()).all()
|
||||
if cur_l:
|
||||
languages[0].name = cur_l.get_language_name(get_locale())
|
||||
else:
|
||||
languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
|
||||
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
|
||||
#if cur_l:
|
||||
# languages[0].name = cur_l.get_language_name(get_locale())
|
||||
#else:
|
||||
# languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
|
||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||
len(languages))
|
||||
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination)
|
||||
@@ -536,11 +533,10 @@ def feed_search(term):
|
||||
|
||||
|
||||
def check_auth(username, password):
|
||||
if sys.version_info.major == 3:
|
||||
try:
|
||||
username = username.encode('windows-1252')
|
||||
except UnicodeEncodeError:
|
||||
username = username.encode('utf-8')
|
||||
try:
|
||||
username = username.encode('windows-1252')
|
||||
except UnicodeEncodeError:
|
||||
username = username.encode('utf-8')
|
||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
|
||||
username.decode('utf-8').lower()).first()
|
||||
if bool(user and check_password_hash(str(user.password), password)):
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
from math import ceil
|
||||
|
||||
|
||||
|
||||
+2
-5
@@ -27,11 +27,8 @@
|
||||
|
||||
# http://flask.pocoo.org/snippets/62/
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
try:
|
||||
from urllib.parse import urlparse, urljoin
|
||||
except ImportError:
|
||||
from urlparse import urlparse, urljoin
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
|
||||
from flask import request, url_for, redirect
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
#
|
||||
# Inspired by http://flask.pocoo.org/snippets/35/
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
|
||||
class ReverseProxied(object):
|
||||
"""Wrap the application in this middleware and configure the
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2021 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import json
|
||||
import importlib
|
||||
import sys
|
||||
import inspect
|
||||
import datetime
|
||||
import concurrent.futures
|
||||
|
||||
from flask import Blueprint, request, Response, url_for
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||
|
||||
from . import constants, logger, ub
|
||||
from cps.services.Metadata import Metadata
|
||||
|
||||
|
||||
meta = Blueprint('metadata', __name__)
|
||||
|
||||
log = logger.create()
|
||||
|
||||
new_list = list()
|
||||
meta_dir = os.path.join(constants.BASE_DIR, "cps", "metadata_provider")
|
||||
modules = os.listdir(os.path.join(constants.BASE_DIR, "cps", "metadata_provider"))
|
||||
for f in modules:
|
||||
if os.path.isfile(os.path.join(meta_dir, f)) and not f.endswith('__init__.py'):
|
||||
a = os.path.basename(f)[:-3]
|
||||
try:
|
||||
importlib.import_module("cps.metadata_provider." + a)
|
||||
new_list.append(a)
|
||||
except ImportError:
|
||||
log.error("Import error for metadata source: {}".format(a))
|
||||
pass
|
||||
|
||||
def list_classes(provider_list):
|
||||
classes = list()
|
||||
for element in provider_list:
|
||||
for name, obj in inspect.getmembers(sys.modules["cps.metadata_provider." + element]):
|
||||
if inspect.isclass(obj) and name != "Metadata" and issubclass(obj, Metadata):
|
||||
classes.append(obj())
|
||||
return classes
|
||||
|
||||
cl = list_classes(new_list)
|
||||
|
||||
@meta.route("/metadata/provider")
|
||||
@login_required
|
||||
def metadata_provider():
|
||||
active = current_user.view_settings.get('metadata', {})
|
||||
provider = list()
|
||||
for c in cl:
|
||||
ac = active.get(c.__id__, True)
|
||||
provider.append({"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__})
|
||||
return Response(json.dumps(provider), mimetype='application/json')
|
||||
|
||||
@meta.route("/metadata/provider", methods=['POST'])
|
||||
@meta.route("/metadata/provider/<prov_name>", methods=['POST'])
|
||||
@login_required
|
||||
def metadata_change_active_provider(prov_name):
|
||||
new_state = request.get_json()
|
||||
active = current_user.view_settings.get('metadata', {})
|
||||
active[new_state['id']] = new_state['value']
|
||||
current_user.view_settings['metadata'] = active
|
||||
try:
|
||||
try:
|
||||
flag_modified(current_user, "view_settings")
|
||||
except AttributeError:
|
||||
pass
|
||||
ub.session.commit()
|
||||
except (InvalidRequestError, OperationalError):
|
||||
log.error("Invalid request received: {}".format(request))
|
||||
return "Invalid request", 400
|
||||
if "initial" in new_state and prov_name:
|
||||
for c in cl:
|
||||
if c.__id__ == prov_name:
|
||||
data = c.search(new_state.get('query', ""))
|
||||
break
|
||||
return Response(json.dumps(data), mimetype='application/json')
|
||||
return ""
|
||||
|
||||
@meta.route("/metadata/search", methods=['POST'])
|
||||
@login_required
|
||||
def metadata_search():
|
||||
query = request.form.to_dict().get('query')
|
||||
data = list()
|
||||
active = current_user.view_settings.get('metadata', {})
|
||||
if query:
|
||||
generic_cover = ""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
meta = {executor.submit(c.search, query, generic_cover): c for c in cl if active.get(c.__id__, True)}
|
||||
for future in concurrent.futures.as_completed(meta):
|
||||
data.extend(future.result())
|
||||
return Response(json.dumps(data), mimetype='application/json')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import errno
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2021 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class Metadata():
|
||||
__name__ = "Generic"
|
||||
|
||||
def __init__(self):
|
||||
self.active = True
|
||||
|
||||
def set_status(self, state):
|
||||
self.active = state
|
||||
+21
-19
@@ -21,11 +21,8 @@ import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from jsonschema import validate, exceptions, __version__
|
||||
from datetime import datetime
|
||||
try:
|
||||
# pylint: disable=unused-import
|
||||
from urllib import unquote
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import json
|
||||
from .. import logger
|
||||
@@ -35,10 +32,7 @@ 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())
|
||||
return b64encode(json.dumps(json_data).encode())
|
||||
|
||||
|
||||
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
|
||||
@@ -85,8 +79,8 @@ class SyncToken:
|
||||
"books_last_created": {"type": "string"},
|
||||
"archive_last_modified": {"type": "string"},
|
||||
"reading_state_last_modified": {"type": "string"},
|
||||
"tags_last_modified": {"type": "string"},
|
||||
"books_last_id": {"type": "integer", "optional": True}
|
||||
"tags_last_modified": {"type": "string"}
|
||||
# "books_last_id": {"type": "integer", "optional": True}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -97,8 +91,8 @@ class SyncToken:
|
||||
books_last_modified=datetime.min,
|
||||
archive_last_modified=datetime.min,
|
||||
reading_state_last_modified=datetime.min,
|
||||
tags_last_modified=datetime.min,
|
||||
books_last_id=-1
|
||||
tags_last_modified=datetime.min
|
||||
# books_last_id=-1
|
||||
): # nosec
|
||||
self.raw_kobo_store_token = raw_kobo_store_token
|
||||
self.books_last_created = books_last_created
|
||||
@@ -106,7 +100,7 @@ class SyncToken:
|
||||
self.archive_last_modified = archive_last_modified
|
||||
self.reading_state_last_modified = reading_state_last_modified
|
||||
self.tags_last_modified = tags_last_modified
|
||||
self.books_last_id = books_last_id
|
||||
# self.books_last_id = books_last_id
|
||||
|
||||
@staticmethod
|
||||
def from_headers(headers):
|
||||
@@ -141,12 +135,12 @@ class SyncToken:
|
||||
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")
|
||||
books_last_id = data_json["books_last_id"]
|
||||
# books_last_id = data_json["books_last_id"]
|
||||
except TypeError:
|
||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||
except KeyError:
|
||||
books_last_id = -1
|
||||
#except KeyError:
|
||||
# books_last_id = -1
|
||||
|
||||
return SyncToken(
|
||||
raw_kobo_store_token=raw_kobo_store_token,
|
||||
@@ -155,7 +149,7 @@ class SyncToken:
|
||||
archive_last_modified=archive_last_modified,
|
||||
reading_state_last_modified=reading_state_last_modified,
|
||||
tags_last_modified=tags_last_modified,
|
||||
books_last_id=books_last_id
|
||||
#books_last_id=books_last_id
|
||||
)
|
||||
|
||||
def set_kobo_store_header(self, store_headers):
|
||||
@@ -179,7 +173,15 @@ class SyncToken:
|
||||
"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),
|
||||
"books_last_id":self.books_last_id
|
||||
#"books_last_id":self.books_last_id
|
||||
},
|
||||
}
|
||||
return b64encode_json(token)
|
||||
|
||||
def __str__(self):
|
||||
return "{},{},{},{},{},{}".format(self.books_last_created,
|
||||
self.books_last_modified,
|
||||
self.archive_last_modified,
|
||||
self.reading_state_last_modified,
|
||||
self.tags_last_modified, self.raw_kobo_store_token)
|
||||
#self.books_last_id)
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
from .. import logger
|
||||
|
||||
|
||||
|
||||
+20
-3
@@ -1,4 +1,21 @@
|
||||
from __future__ import print_function
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2021 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os.path
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from google.auth.transport.requests import Request
|
||||
@@ -61,7 +78,7 @@ def get_user_info(credentials):
|
||||
return user_info.get('email', "")
|
||||
|
||||
def send_messsage(token, msg):
|
||||
log.debug("Start sending email via Gmail")
|
||||
log.debug("Start sending e-mail via Gmail")
|
||||
creds = Credentials(
|
||||
token=token['token'],
|
||||
refresh_token=token['refresh_token'],
|
||||
@@ -80,4 +97,4 @@ def send_messsage(token, msg):
|
||||
body = {'raw': raw}
|
||||
|
||||
(service.users().messages().send(userId='me', body=body).execute())
|
||||
log.debug("Email send successfully via Gmail")
|
||||
log.debug("E-mail send successfully via Gmail")
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import time
|
||||
from functools import reduce
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import base64
|
||||
|
||||
from flask_simpleldap import LDAP, LDAPException
|
||||
|
||||
+25
-6
@@ -1,5 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2020 pwr
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import threading
|
||||
import abc
|
||||
import uuid
|
||||
@@ -69,7 +85,7 @@ class WorkerThread(threading.Thread):
|
||||
def add(cls, user, task):
|
||||
ins = cls.getInstance()
|
||||
ins.num += 1
|
||||
log.debug("Add Task for user: {}: {}".format(user, task))
|
||||
log.debug("Add Task for user: {} - {}".format(user, task))
|
||||
ins.queue.put(QueuedTask(
|
||||
num=ins.num,
|
||||
user=user,
|
||||
@@ -205,10 +221,13 @@ class CalibreTask:
|
||||
# By default, we're good to clean a task if it's "Done"
|
||||
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
||||
|
||||
@progress.setter
|
||||
def progress(self, x):
|
||||
# todo: throw error if outside of [0,1]
|
||||
self._progress = x
|
||||
'''@progress.setter
|
||||
def progress(self, x):
|
||||
if x > 1:
|
||||
x = 1
|
||||
if x < 0:
|
||||
x = 0
|
||||
self._progress = x'''
|
||||
|
||||
def _handleError(self, error_message):
|
||||
self.stat = STAT_FAIL
|
||||
|
||||
+71
-51
@@ -20,21 +20,19 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
from datetime import datetime
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, request, flash, redirect, url_for
|
||||
from flask import Blueprint, flash, redirect, request, url_for
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import login_required, current_user
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||
from sqlalchemy.sql.expression import func, true
|
||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||
|
||||
from . import logger, ub, calibre_db, db
|
||||
from . import calibre_db, config, db, logger, ub
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
|
||||
|
||||
shelf = Blueprint('shelf', __name__)
|
||||
log = logger.create()
|
||||
|
||||
@@ -58,7 +56,7 @@ def check_shelf_view_permissions(cur_shelf):
|
||||
return True
|
||||
|
||||
|
||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
|
||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
||||
@login_required
|
||||
def add_to_shelf(shelf_id, book_id):
|
||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
@@ -72,10 +70,9 @@ def add_to_shelf(shelf_id, book_id):
|
||||
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
if not xhr:
|
||||
flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name),
|
||||
category="error")
|
||||
flash(_(u"Sorry you are not allowed to add a book to that shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403
|
||||
return "Sorry you are not allowed to add a book to the that shelf", 403
|
||||
|
||||
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
||||
ub.BookShelf.book_id == book_id).first()
|
||||
@@ -115,7 +112,7 @@ def add_to_shelf(shelf_id, book_id):
|
||||
return "", 204
|
||||
|
||||
|
||||
@shelf.route("/shelf/massadd/<int:shelf_id>")
|
||||
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
|
||||
@login_required
|
||||
def search_to_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
@@ -125,8 +122,8 @@ def search_to_shelf(shelf_id):
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
log.warning("You are not allowed to add a book to the the shelf: {}".format(shelf.name))
|
||||
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
|
||||
log.warning("You are not allowed to add a book to the shelf".format(shelf.name))
|
||||
flash(_(u"You are not allowed to add a book to the shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
|
||||
@@ -167,7 +164,7 @@ def search_to_shelf(shelf_id):
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
|
||||
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>")
|
||||
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
||||
@login_required
|
||||
def remove_from_shelf(shelf_id, book_id):
|
||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
@@ -218,48 +215,61 @@ def remove_from_shelf(shelf_id, book_id):
|
||||
else:
|
||||
if not xhr:
|
||||
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
|
||||
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
|
||||
flash(_(u"Sorry you are not allowed to remove a book from this shelf"),
|
||||
category="error")
|
||||
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", 403
|
||||
|
||||
|
||||
@shelf.route("/shelf/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_shelf():
|
||||
shelf = ub.Shelf()
|
||||
return create_edit_shelf(shelf, title=_(u"Create a Shelf"), page="shelfcreate")
|
||||
return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate")
|
||||
|
||||
|
||||
|
||||
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
return create_edit_shelf(shelf, title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
flash(_(u"Sorry you are not allowed to edit this shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return create_edit_shelf(shelf, page_title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
|
||||
|
||||
|
||||
# if shelf ID is set, we are editing a shelf
|
||||
def create_edit_shelf(shelf, title, page, shelf_id=False):
|
||||
def create_edit_shelf(shelf, page_title, page, shelf_id=False):
|
||||
sync_only_selected_shelves = current_user.kobo_only_shelves_sync
|
||||
# calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count()
|
||||
if request.method == "POST":
|
||||
to_save = request.form.to_dict()
|
||||
if "is_public" in to_save:
|
||||
shelf.is_public = 1
|
||||
else:
|
||||
shelf.is_public = 0
|
||||
if check_shelf_is_unique(shelf, to_save, shelf_id):
|
||||
shelf.name = to_save["title"]
|
||||
# shelf.last_modified = datetime.utcnow()
|
||||
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
|
||||
flash(_(u"Sorry you are not allowed to create a public shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
is_public = 1 if to_save.get("is_public") == "on" else 0
|
||||
if config.config_kobo_sync:
|
||||
shelf.kobo_sync = True if to_save.get("kobo_sync") else False
|
||||
if shelf.kobo_sync:
|
||||
ub.session.query(ub.ShelfArchive).filter(ub.ShelfArchive.user_id == current_user.id).filter(
|
||||
ub.ShelfArchive.uuid == shelf.uuid).delete()
|
||||
ub.session_commit()
|
||||
shelf_title = to_save.get("title", "")
|
||||
if check_shelf_is_unique(shelf, shelf_title, is_public, shelf_id):
|
||||
shelf.name = shelf_title
|
||||
shelf.is_public = is_public
|
||||
if not shelf_id:
|
||||
shelf.user_id = int(current_user.id)
|
||||
ub.session.add(shelf)
|
||||
shelf_action = "created"
|
||||
flash_text = _(u"Shelf %(title)s created", title=to_save["title"])
|
||||
flash_text = _(u"Shelf %(title)s created", title=shelf_title)
|
||||
else:
|
||||
shelf_action = "changed"
|
||||
flash_text = _(u"Shelf %(title)s changed", title=to_save["title"])
|
||||
flash_text = _(u"Shelf %(title)s changed", title=shelf_title)
|
||||
try:
|
||||
ub.session.commit()
|
||||
log.info(u"Shelf {} {}".format(to_save["title"], shelf_action))
|
||||
log.info(u"Shelf {} {}".format(shelf_title, shelf_action))
|
||||
flash(flash_text, category="success")
|
||||
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
||||
except (OperationalError, InvalidRequestError) as ex:
|
||||
@@ -271,34 +281,39 @@ def create_edit_shelf(shelf, title, page, shelf_id=False):
|
||||
ub.session.rollback()
|
||||
log.debug_or_exception(ex)
|
||||
flash(_(u"There was an error"), category="error")
|
||||
return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page)
|
||||
return render_title_template('shelf_edit.html',
|
||||
shelf=shelf,
|
||||
title=page_title,
|
||||
page=page,
|
||||
kobo_sync_enabled=config.config_kobo_sync,
|
||||
sync_only_selected_shelves=sync_only_selected_shelves)
|
||||
|
||||
|
||||
def check_shelf_is_unique(shelf, to_save, shelf_id=False):
|
||||
def check_shelf_is_unique(shelf, title, is_public, shelf_id=False):
|
||||
if shelf_id:
|
||||
ident = ub.Shelf.id != shelf_id
|
||||
else:
|
||||
ident = true()
|
||||
if shelf.is_public == 1:
|
||||
if is_public == 1:
|
||||
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1)) \
|
||||
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \
|
||||
.filter(ident) \
|
||||
.first() is None
|
||||
|
||||
if not is_shelf_name_unique:
|
||||
log.error("A public shelf with the name '{}' already exists.".format(to_save["title"]))
|
||||
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
||||
log.error("A public shelf with the name '{}' already exists.".format(title))
|
||||
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=title),
|
||||
category="error")
|
||||
else:
|
||||
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) &
|
||||
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 0) &
|
||||
(ub.Shelf.user_id == int(current_user.id))) \
|
||||
.filter(ident) \
|
||||
.first() is None
|
||||
|
||||
if not is_shelf_name_unique:
|
||||
log.error("A private shelf with the name '{}' already exists.".format(to_save["title"]))
|
||||
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
||||
log.error("A private shelf with the name '{}' already exists.".format(title))
|
||||
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=title),
|
||||
category="error")
|
||||
return is_shelf_name_unique
|
||||
|
||||
@@ -313,12 +328,13 @@ def delete_shelf_helper(cur_shelf):
|
||||
ub.session_commit("successfully deleted Shelf {}".format(cur_shelf.name))
|
||||
|
||||
|
||||
@shelf.route("/shelf/delete/<int:shelf_id>")
|
||||
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_shelf(shelf_id):
|
||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
try:
|
||||
delete_shelf_helper(cur_shelf)
|
||||
flash(_("Shelf successfully deleted"), category="success")
|
||||
except InvalidRequestError:
|
||||
ub.session.rollback()
|
||||
log.error("Settings DB is not Writeable")
|
||||
@@ -362,8 +378,8 @@ def order_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
result = list()
|
||||
if shelf and check_shelf_view_permissions(shelf):
|
||||
result = calibre_db.session.query(db.Books)\
|
||||
.join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \
|
||||
result = calibre_db.session.query(db.Books) \
|
||||
.join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id, isouter=True) \
|
||||
.add_columns(calibre_db.common_filters().label("visible")) \
|
||||
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
|
||||
return render_title_template('shelf_order.html', entries=result,
|
||||
@@ -372,7 +388,9 @@ def order_shelf(shelf_id):
|
||||
|
||||
|
||||
def change_shelf_order(shelf_id, order):
|
||||
result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\
|
||||
result = calibre_db.session.query(db.Books).outerjoin(db.books_series_link,
|
||||
db.Books.id == db.books_series_link.c.book)\
|
||||
.outerjoin(db.Series).join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id) \
|
||||
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
|
||||
for index, entry in enumerate(result):
|
||||
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||
@@ -402,9 +420,11 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
||||
if sort_param == 'old':
|
||||
change_shelf_order(shelf_id, [db.Books.timestamp])
|
||||
if sort_param == 'authaz':
|
||||
change_shelf_order(shelf_id, [db.Books.author_sort.asc()])
|
||||
change_shelf_order(shelf_id, [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index])
|
||||
if sort_param == 'authza':
|
||||
change_shelf_order(shelf_id, [db.Books.author_sort.desc()])
|
||||
change_shelf_order(shelf_id, [db.Books.author_sort.desc(),
|
||||
db.Series.name.desc(),
|
||||
db.Books.series_index.desc()])
|
||||
page = "shelf.html"
|
||||
pagesize = 0
|
||||
else:
|
||||
@@ -412,13 +432,13 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
||||
page = 'shelfdown.html'
|
||||
|
||||
result, __, pagination = calibre_db.fill_indexpage(page_no, pagesize,
|
||||
db.Books,
|
||||
ub.BookShelf.shelf == shelf_id,
|
||||
[ub.BookShelf.order.asc()],
|
||||
ub.BookShelf,ub.BookShelf.book_id == db.Books.id)
|
||||
db.Books,
|
||||
ub.BookShelf.shelf == shelf_id,
|
||||
[ub.BookShelf.order.asc()],
|
||||
ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
|
||||
# delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
|
||||
wrong_entries = calibre_db.session.query(ub.BookShelf)\
|
||||
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\
|
||||
wrong_entries = calibre_db.session.query(ub.BookShelf) \
|
||||
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True) \
|
||||
.filter(db.Books.id == None).all()
|
||||
for entry in wrong_entries:
|
||||
log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf))
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user