Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
776eeccc67 | ||
|
|
91e36b2c7e | ||
|
|
4fe28028ea | ||
|
|
e2c88b3a33 | ||
|
|
f58c8eb916 | ||
|
|
6f5af36ecf | ||
|
|
7c1d372086 | ||
|
|
37b9ee9b0d | ||
|
|
bcfdeacf15 | ||
|
|
cb588c2c82 | ||
|
|
0b777c47bb | ||
|
|
4dce8abb1d | ||
|
|
f27d62e22c | ||
|
|
0eb8583dce | ||
|
|
6f969ed6a6 | ||
|
|
707c41c48a | ||
|
|
054256067a | ||
|
|
8c0891a078 | ||
|
|
cc0fbd7ce8 | ||
|
|
50be750460 | ||
|
|
5127061775 | ||
|
|
0d2611c8a0 | ||
|
|
521b46e60d | ||
|
|
36a99b0b5c | ||
|
|
4e67a56ed2 | ||
|
|
2b2ccb1710 | ||
|
|
4583fc8023 | ||
|
|
2c9d4d2487 | ||
|
|
af09b50153 | ||
|
|
b8c3674be9 | ||
|
|
65a29ed4c9 | ||
|
|
854a7d020d | ||
|
|
a215b73e39 | ||
|
|
6bfe022de0 | ||
|
|
c6cde961b6 | ||
|
|
86e6161b89 | ||
|
|
646937272a | ||
|
|
6dd7a023f3 | ||
|
|
f0077e73a8 | ||
|
|
e485f10e9e | ||
|
|
81a0b15a3d | ||
|
|
89577914ba | ||
|
|
c289cb5d4b | ||
|
|
80bcfb12de | ||
|
|
46f111cd6e | ||
|
|
3d9594cfaa | ||
|
|
bc0e218e73 | ||
|
|
f9267ba391 | ||
|
|
eb02c9e5d4 | ||
|
|
22a93a28e2 | ||
|
|
1ee49791f4 | ||
|
|
90bccb86ef | ||
|
|
ff6b341c36 | ||
|
|
9d3b2b007b | ||
|
|
efc7ce31d2 | ||
|
|
4ccc2c0aec | ||
|
|
4512ab4524 | ||
|
|
0ef9e46133 | ||
|
|
88a06deb8c | ||
|
|
e4f1ce9c5a | ||
|
|
f5048ee1a8 | ||
|
|
e7c2a85fd4 | ||
|
|
ee7f393e67 | ||
|
|
71686e4490 | ||
|
|
ea1ba54a9d | ||
|
|
659535f5de | ||
|
|
d977d1b81b | ||
|
|
01215aaa68 | ||
|
|
ed600a9aeb | ||
|
|
e542151652 | ||
|
|
e49402649d | ||
|
|
21989c3ccf | ||
|
|
42924d9508 | ||
|
|
f9390ceea5 | ||
|
|
dc7a0575dd | ||
|
|
a972447dbc | ||
|
|
87b200cc13 | ||
|
|
14e57e3714 | ||
|
|
d45ed1c921 | ||
|
|
344c0c7bc3 | ||
|
|
190ee09e60 | ||
|
|
e6031844d5 | ||
|
|
7c6347442f | ||
|
|
16255870a8 | ||
|
|
65bcffc129 | ||
|
|
b28547b248 | ||
|
|
8873381290 | ||
|
|
ebb7ddec44 | ||
|
|
b07541a251 | ||
|
|
85a66a3bfe | ||
|
|
f493d4e4e5 | ||
|
|
5d8908db49 | ||
|
|
14b044d055 | ||
|
|
9b0b82ec1b | ||
|
|
881b48729c | ||
|
|
812265324d | ||
|
|
621f8debea | ||
|
|
43040f6162 | ||
|
|
0bf3bb381f | ||
|
|
2bfbbfd4ba | ||
|
|
6cb1449ff3 | ||
|
|
92fbc9c792 | ||
|
|
f9cfb38ad3 | ||
|
|
e5dc7e2834 | ||
|
|
38a7454e7a | ||
|
|
985e59ad68 | ||
|
|
5334acac37 | ||
|
|
85368c89dd | ||
|
|
75499b989c | ||
|
|
366a65f99f | ||
|
|
fea8a9d40d | ||
|
|
0e9ad0f831 | ||
|
|
c53a516887 | ||
|
|
9955e348a0 | ||
|
|
26fb9921b2 | ||
|
|
ba383643f5 | ||
|
|
6bd01c2d2b | ||
|
|
26c8e15436 | ||
|
|
fd993b5063 |
@@ -11,7 +11,7 @@ assignees: ''
|
||||
|
||||
After 6 years of more or less intensive programming on Calibre-Web, I need a break.
|
||||
The last few months, maintaining Calibre-Web has felt more like work than a hobby. I felt pressured and teased by people to solve "their" problems and merge PRs for "their" Calibre-Web.
|
||||
I have turned off all notifications from Github/Discord and will now concentrate undisturbed on the development of “my” Calibre-Web over the next few weeks/months.
|
||||
I have turned off all notifications from GitHub/Discord and will now concentrate undisturbed on the development of “my” Calibre-Web over the next few weeks/months.
|
||||
I will look into the issues and maybe also the PRs from time to time, but don't expect a quick response from me.
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ assignees: ''
|
||||
|
||||
After 6 years of more or less intensive programming on Calibre-Web, I need a break.
|
||||
The last few months, maintaining Calibre-Web has felt more like work than a hobby. I felt pressured and teased by people to solve "their" problems and merge PRs for "their" Calibre-Web.
|
||||
I have turned off all notifications from Github/Discord and will now concentrate undisturbed on the development of “my” Calibre-Web over the next few weeks/months.
|
||||
I have turned off all notifications from GitHub/Discord and will now concentrate undisturbed on the development of “my” Calibre-Web over the next few weeks/months.
|
||||
I will look into the issues and maybe also the PRs from time to time, but don't expect a quick response from me.
|
||||
|
||||
Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
|
||||
|
||||
+4
-4
@@ -20,7 +20,7 @@ Some of the user languages in Calibre-Web having missing translations. We are ha
|
||||
|
||||
### **Documentation**
|
||||
|
||||
The Calibre-Web documentation is hosted in the Github [Wiki](https://github.com/janeczku/calibre-web/wiki). The Wiki is open to everybody, if you find a problem, feel free to correct it. If information is missing, you are welcome to add it. The content will be reviewed time by time. Please try to be consistent with the form with the other Wiki pages (e.g. the project name is Calibre-Web with 2 capital letters and a dash in between).
|
||||
The Calibre-Web documentation is hosted in the GitHub [Wiki](https://github.com/janeczku/calibre-web/wiki). The Wiki is open to everybody, if you find a problem, feel free to correct it. If information is missing, you are welcome to add it. The content will be reviewed time by time. Please try to be consistent with the form with the other Wiki pages (e.g. the project name is Calibre-Web with 2 capital letters and a dash in between).
|
||||
|
||||
### **Reporting a bug**
|
||||
|
||||
@@ -28,12 +28,12 @@ Do not open up a GitHub issue if the bug is a **security vulnerability** in Cali
|
||||
|
||||
Ensure the **bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
|
||||
|
||||
If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new/choose). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you providing the right information. Deleting the form and just pasting the stack trace doesn't speed up fixing the problem. If your issue could be resolved, consider closing the issue.
|
||||
If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new/choose). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you provide the right information. Deleting the form and just pasting the stack trace doesn't speed up fixing the problem. If your issue could be resolved, consider closing the issue.
|
||||
|
||||
### **Feature Request**
|
||||
|
||||
If there is a feature missing in Calibre-Web and you can't find a feature request in the [Issues](https://github.com/janeczku/calibre-web/issues) section, you could create a [feature request](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=feature_request.md&title=).
|
||||
We will not extend Calibre-Web with any more login abilities or add further files storages, or file syncing ability. Furthermore Calibre-Web is made for home usage for company in-house usage, so requests regarding any sorts of social interaction capability, payment routines, search engine or web site analytics integration will not be implemented.
|
||||
We will not extend Calibre-Web with any more login abilities or add further files storages, or file syncing ability. Calibre-Web is made for home usage for company in-house usage, so requests regarding any sorts of social interaction capability, payment routines, search engine or website analytics integration will not be implemented.
|
||||
|
||||
### **Contributing code to Calibre-Web**
|
||||
|
||||
@@ -42,5 +42,5 @@ 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 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.
|
||||
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.
|
||||
|
||||
@@ -115,7 +115,6 @@ Calibre-Web is a web app that offers a clean and intuitive interface for browsin
|
||||
|
||||
- **Python Version**: Ensure you have Python 3.7 or newer.
|
||||
- **Imagemagick**: Required for cover extraction from EPUBs. Windows users may also need to install [Ghostscript](https://ghostscript.com/releases/gsdnld.html) for PDF cover extraction.
|
||||
- **Windows Users**: Install [libmagic](https://gnuwin32.sourceforge.net/downlinks/file.php) for 32bit python or [libmagic for 64bit python](https://github.com/nscaife/file-windows/releases/tag/20170108), depending on your Python version. Make sure these files are included in your path.
|
||||
- **Optional Tools**:
|
||||
- **Calibre desktop program**: Recommended for on-the-fly conversion and metadata editing. Set the path to Calibre’s converter tool on the setup page.
|
||||
- **Kepubify tool**: Needed for Kobo device support. Download the tool and place the binary in `/opt/kepubify` on Linux or `C:\Program Files\kepubify` on Windows.
|
||||
|
||||
+38
-38
@@ -10,44 +10,44 @@ To receive fixes for security vulnerabilities it is required to always upgrade t
|
||||
|
||||
## 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 |CVE-2021-4164|
|
||||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 |CVE-2021-4170|
|
||||
| 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 |CVE-2022-0352|
|
||||
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 |CVE-2022-0339|
|
||||
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @nhiephon |CVE-2022-0273|
|
||||
| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon |CVE-2022-0405|
|
||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 |CVE-2022-0767|
|
||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH |CVE-2022-0766|
|
||||
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) |CVE-2022-30765|
|
||||
| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 |CVE-2022-0939|
|
||||
| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley |CVE-2022-0990|
|
||||
| V 0.6.20 | Credentials for emails are now stored encrypted ||
|
||||
| V 0.6.20 | Login is rate limited ||
|
||||
| V 0.6.20 | Passwordstrength can be forced ||
|
||||
| V 0.6.21 | SMTP server credentials are no longer returned to client ||
|
||||
| V 0.6.21 | Cross-site scripting (XSS) stored in href bypasses filter using data wrapper no longer possible ||
|
||||
| V 0.6.21 | Cross-site scripting (XSS) is no longer possible via pathchooser ||
|
||||
| V 0.6.21 | Error Handling at non existent rating, language, and user downloaded books was fixed ||
|
||||
| V 0.6.22 | Upload mimetype is checked to prevent malicious file content in the books library ||
|
||||
| V 0.6.22 | Cross-site scripting (XSS) stored in comments section is prevented better (switching from lxml to bleach for sanitizing strings) ||
|
||||
| V 0.6.23 | Cookies are no longer stored for opds basic authentication and proxy authentication ||
|
||||
| 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 | CVE-2021-4164 |
|
||||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 | CVE-2021-4170 |
|
||||
| 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 | CVE-2022-0352 |
|
||||
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 | CVE-2022-0339 |
|
||||
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @nhiephon | CVE-2022-0273 |
|
||||
| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon | CVE-2022-0405 |
|
||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 | CVE-2022-0767 |
|
||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH | CVE-2022-0766 |
|
||||
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) | CVE-2022-30765 |
|
||||
| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 | CVE-2022-0939 |
|
||||
| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley | CVE-2022-0990 |
|
||||
| V 0.6.20 | Credentials for emails are now stored encrypted | |
|
||||
| V 0.6.20 | Login is rate limited | |
|
||||
| V 0.6.20 | Passwordstrength can be forced | |
|
||||
| V 0.6.21 | SMTP server credentials are no longer returned to client | |
|
||||
| V 0.6.21 | Cross-site scripting (XSS) stored in href bypasses filter using data wrapper no longer possible | |
|
||||
| V 0.6.21 | Cross-site scripting (XSS) is no longer possible via pathchooser | |
|
||||
| V 0.6.21 | Error Handling at non existent rating, language, and user downloaded books was fixed | |
|
||||
| V 0.6.22 | Upload mimetype is checked to prevent malicious file content in the books library | |
|
||||
| V 0.6.22 | Cross-site scripting (XSS) stored in comments section is prevented better (switching from lxml to bleach for sanitizing strings) | |
|
||||
| V 0.6.23 | Cookies are no longer stored for opds basic authentication and proxy authentication | |
|
||||
|
||||
|
||||
|
||||
|
||||
+12
-13
@@ -55,17 +55,17 @@ mimetypes.init()
|
||||
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
||||
mimetypes.add_type('application/epub+zip', '.epub')
|
||||
mimetypes.add_type('application/epub+zip', '.kepub')
|
||||
mimetypes.add_type('text/xml', '.fb2')
|
||||
mimetypes.add_type('application/octet-stream', '.mobi')
|
||||
mimetypes.add_type('application/fb2+zip', '.fb2')
|
||||
mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
|
||||
mimetypes.add_type('application/octet-stream', '.prc')
|
||||
mimetypes.add_type('application/vnd.amazon.ebook', '.azw')
|
||||
mimetypes.add_type('application/x-mobi8-ebook', '.azw3')
|
||||
mimetypes.add_type('application/x-rar', '.cbr')
|
||||
mimetypes.add_type('application/zip', '.cbz')
|
||||
mimetypes.add_type('application/x-mobipocket-ebook', '.azw')
|
||||
mimetypes.add_type('application/x-mobipocket-ebook', '.azw3')
|
||||
mimetypes.add_type('application/x-cbr', '.cbr')
|
||||
mimetypes.add_type('application/x-cbz', '.cbz')
|
||||
mimetypes.add_type('application/x-tar', '.cbt')
|
||||
mimetypes.add_type('application/x-7z-compressed', '.cb7')
|
||||
mimetypes.add_type('image/vnd.djv', '.djv')
|
||||
mimetypes.add_type('image/vnd.djv', '.djvu')
|
||||
mimetypes.add_type('image/vnd.djvu', '.djv')
|
||||
mimetypes.add_type('image/vnd.djvu', '.djvu')
|
||||
mimetypes.add_type('application/mpeg', '.mpeg')
|
||||
mimetypes.add_type('audio/mpeg', '.mp3')
|
||||
mimetypes.add_type('audio/x-m4a', '.m4a')
|
||||
@@ -77,7 +77,8 @@ mimetypes.add_type('audio/ogg', '.ogg')
|
||||
mimetypes.add_type('application/ogg', '.oga')
|
||||
mimetypes.add_type('text/css', '.css')
|
||||
mimetypes.add_type('application/x-ms-reader', '.lit')
|
||||
mimetypes.add_type('text/javascript; charset=UTF-8', '.js')
|
||||
mimetypes.add_type('text/javascript', '.js')
|
||||
mimetypes.add_type('text/rtf', '.rtf')
|
||||
|
||||
log = logger.create()
|
||||
|
||||
@@ -102,7 +103,7 @@ if wtf_present:
|
||||
else:
|
||||
csrf = None
|
||||
|
||||
calibre_db = db.CalibreDB()
|
||||
calibre_db = db.CalibreDB(app)
|
||||
|
||||
web_server = WebServer()
|
||||
|
||||
@@ -146,9 +147,7 @@ def create_app():
|
||||
lm.anonymous_user = ub.Anonymous
|
||||
lm.session_protection = 'strong' if config.config_session == 1 else "basic"
|
||||
|
||||
db.CalibreDB.update_config(config)
|
||||
db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path)
|
||||
calibre_db.init_db()
|
||||
db.CalibreDB.update_config(config, config.config_calibre_dir, cli_param.settings_path)
|
||||
|
||||
updater_thread.init_updater(config, web_server)
|
||||
# Perform dry run of updater and exit afterward
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@
|
||||
import sys
|
||||
import platform
|
||||
import sqlite3
|
||||
import importlib
|
||||
from importlib.metadata import metadata
|
||||
from collections import OrderedDict
|
||||
|
||||
import flask
|
||||
@@ -41,7 +41,7 @@ req = dep_check.load_dependencies(False)
|
||||
opt = dep_check.load_dependencies(True)
|
||||
for i in (req + opt):
|
||||
modules[i[1]] = i[0]
|
||||
modules['Jinja2'] = importlib.metadata.version("jinja2")
|
||||
modules['Jinja2'] = metadata("jinja2")["Version"]
|
||||
if sys.version_info < (3, 12):
|
||||
modules['pySqlite'] = sqlite3.version
|
||||
modules['SQLite'] = sqlite3.sqlite_version
|
||||
|
||||
+21
-25
@@ -32,7 +32,8 @@ from datetime import time as datetime_time
|
||||
from functools import wraps
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, \
|
||||
send_from_directory, g, jsonify
|
||||
from markupsafe import Markup
|
||||
from .cw_login import current_user
|
||||
from flask_babel import gettext as _
|
||||
@@ -118,7 +119,7 @@ def before_request():
|
||||
g.allow_upload = config.config_uploading
|
||||
g.current_theme = config.config_theme
|
||||
g.config_authors_max = config.config_authors_max
|
||||
if '/static/' not in request.path and not config.db_configured and \
|
||||
if ('/static/' not in request.path and not config.db_configured and
|
||||
request.endpoint not in ('admin.ajax_db_config',
|
||||
'admin.simulatedbchange',
|
||||
'admin.db_configuration',
|
||||
@@ -126,7 +127,7 @@ def before_request():
|
||||
'web.login_post',
|
||||
'web.logout',
|
||||
'admin.load_dialogtexts',
|
||||
'admin.ajax_pathchooser'):
|
||||
'admin.ajax_pathchooser')):
|
||||
return redirect(url_for('admin.db_configuration'))
|
||||
|
||||
|
||||
@@ -144,7 +145,6 @@ def shutdown():
|
||||
show_text = {}
|
||||
if task in (0, 1): # valid commandos received
|
||||
# close all database connections
|
||||
calibre_db.dispose()
|
||||
ub.dispose()
|
||||
|
||||
if task == 0:
|
||||
@@ -189,8 +189,8 @@ def reconnect():
|
||||
|
||||
|
||||
@admi.route("/ajax/updateThumbnails", methods=['POST'])
|
||||
@admin_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def update_thumbnails():
|
||||
content = config.get_scheduled_task_settings()
|
||||
if content['schedule_generate_book_covers']:
|
||||
@@ -379,10 +379,7 @@ def list_users():
|
||||
user.default = get_user_locale_language(user.default_language)
|
||||
|
||||
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users}
|
||||
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
|
||||
response = make_response(js_list)
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
return make_response(json.dumps(table_entries, cls=db.AlchemyEncoder))
|
||||
|
||||
|
||||
@admi.route("/ajax/deleteuser", methods=['POST'])
|
||||
@@ -401,7 +398,7 @@ def delete_user():
|
||||
success = list()
|
||||
if not users:
|
||||
log.error("User not found")
|
||||
return Response(json.dumps({'type': "danger", 'message': _("User not found")}), mimetype='application/json')
|
||||
return make_response(jsonify(type="danger", message=_("User not found")))
|
||||
for user in users:
|
||||
try:
|
||||
message = _delete_user(user)
|
||||
@@ -417,7 +414,7 @@ def delete_user():
|
||||
log.info("Users {} deleted".format(user_ids))
|
||||
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
|
||||
success.extend(errors)
|
||||
return Response(json.dumps(success), mimetype='application/json')
|
||||
return make_response(jsonify(success))
|
||||
|
||||
|
||||
@admi.route("/ajax/getlocale")
|
||||
@@ -429,7 +426,7 @@ def table_get_locale():
|
||||
current_locale = get_locale()
|
||||
for loc in locale:
|
||||
ret.append({'value': str(loc), 'text': loc.get_language_name(current_locale)})
|
||||
return json.dumps(ret)
|
||||
return json.dumps(sorted(ret, key=lambda x: x['text']))
|
||||
|
||||
|
||||
@admi.route("/ajax/getdefaultlanguage")
|
||||
@@ -441,7 +438,7 @@ def table_get_default_lang():
|
||||
ret.append({'value': 'all', 'text': _('Show All')})
|
||||
for lang in languages:
|
||||
ret.append({'value': lang.lang_code, 'text': lang.name})
|
||||
return json.dumps(ret)
|
||||
return json.dumps(sorted(ret, key=lambda x: x['text']))
|
||||
|
||||
|
||||
@admi.route("/ajax/editlistusers/<param>", methods=['POST'])
|
||||
@@ -499,10 +496,10 @@ def edit_list_user(param):
|
||||
if not ub.session.query(ub.User). \
|
||||
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||
ub.User.id != user.id).count():
|
||||
return Response(
|
||||
json.dumps([{'type': "danger",
|
||||
return make_response(
|
||||
jsonify([{'type': "danger",
|
||||
'message': _("No admin user remaining, can't remove admin role",
|
||||
nick=user.name)}]), mimetype='application/json')
|
||||
nick=user.name)}]))
|
||||
user.role &= ~value
|
||||
else:
|
||||
raise Exception(_("Value has to be true or false"))
|
||||
@@ -948,7 +945,7 @@ def do_full_kobo_sync(userid):
|
||||
count = ub.session.query(ub.KoboSyncedBooks).filter(userid == 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')
|
||||
return make_response(jsonify(type="success", message=message))
|
||||
|
||||
|
||||
def check_valid_read_column(column):
|
||||
@@ -1265,7 +1262,7 @@ def _configuration_ldap_helper(to_save):
|
||||
@admin_required
|
||||
def simulatedbchange():
|
||||
db_change, db_valid = _db_simulate_change()
|
||||
return Response(json.dumps({"change": db_change, "valid": db_valid}), mimetype='application/json')
|
||||
return make_response(jsonify(change=db_change, valid=db_valid))
|
||||
|
||||
|
||||
@admi.route("/admin/user/new", methods=["GET", "POST"])
|
||||
@@ -1737,14 +1734,12 @@ def _db_configuration_update_helper():
|
||||
return _db_configuration_result(_("Books path not valid"), gdrive_error)
|
||||
else:
|
||||
_config_string(to_save, "config_calibre_split_dir")
|
||||
|
||||
if db_change or not db_valid or not config.db_configured \
|
||||
or config.config_calibre_dir != to_save["config_calibre_dir"]:
|
||||
if (db_change or not db_valid or not config.db_configured
|
||||
or config.config_calibre_dir != to_save["config_calibre_dir"]):
|
||||
if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']:
|
||||
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error)
|
||||
else:
|
||||
calibre_db.setup_db(to_save['config_calibre_dir'], ub.app_DB_path)
|
||||
config.store_calibre_uuid(calibre_db, db.Library_Id)
|
||||
# if db changed -> delete shelfs, delete download books, delete read books, kobo sync...
|
||||
if db_change:
|
||||
log.info("Calibre Database changed, all Calibre-Web info related to old Database gets deleted")
|
||||
@@ -1767,10 +1762,11 @@ def _db_configuration_update_helper():
|
||||
config.config_allowed_column_value = ""
|
||||
config.config_read_column = 0
|
||||
_config_string(to_save, "config_calibre_dir")
|
||||
calibre_db.update_config(config)
|
||||
calibre_db.update_config(config, config.config_calibre_dir, ub.app_DB_path)
|
||||
config.store_calibre_uuid(calibre_db, db.Library_Id)
|
||||
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
||||
flash(_("DB is not Writeable"), category="warning")
|
||||
calibre_db.update_config(config)
|
||||
calibre_db.update_config(config, config.config_calibre_dir, ub.app_DB_path)
|
||||
config.save()
|
||||
return _db_configuration_result(None, gdrive_error)
|
||||
|
||||
@@ -1898,7 +1894,7 @@ def _configuration_result(error_flash=None, reboot=False):
|
||||
resp['result'] = [{'type': "success", 'message': _("Calibre-Web configuration updated")}]
|
||||
resp['reboot'] = reboot
|
||||
resp['config_upload'] = config.config_upload_formats
|
||||
return Response(json.dumps(resp), mimetype='application/json')
|
||||
return make_response(jsonify(resp))
|
||||
|
||||
|
||||
def _db_configuration_result(error_flash=None, gdrive_error=None):
|
||||
|
||||
+6
-12
@@ -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/>.
|
||||
|
||||
import os
|
||||
|
||||
import mutagen
|
||||
import base64
|
||||
from . import cover, logger
|
||||
@@ -51,13 +49,12 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na
|
||||
if not pubdate:
|
||||
pubdate = str(audio_file.tags.get('TDOR').text[0]) if "TDOR" in audio_file.tags else None
|
||||
if cover_data and not no_cover_processing:
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||
cover_info = cover_data[0]
|
||||
for dat in cover_data:
|
||||
if dat.type == mutagen.id3.PictureType.COVER_FRONT:
|
||||
cover_info = dat
|
||||
break
|
||||
cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||
tmp_cover_name = cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||
elif original_file_extension in [".ogg", ".flac", ".opus", ".ogv"]:
|
||||
title = audio_file.tags.get('TITLE')[0] if "TITLE" in audio_file else None
|
||||
author = audio_file.tags.get('ARTIST')[0] if "ARTIST" in audio_file else None
|
||||
@@ -70,17 +67,15 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na
|
||||
cover_data = audio_file.tags.get('METADATA_BLOCK_PICTURE')
|
||||
if not no_cover_processing:
|
||||
if cover_data:
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||
cover_info = mutagen.flac.Picture(base64.b64decode(cover_data[0]))
|
||||
cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||
tmp_cover_name = cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||
if hasattr(audio_file, "pictures"):
|
||||
cover_info = audio_file.pictures[0]
|
||||
for dat in audio_file.pictures:
|
||||
if dat.type == mutagen.id3.PictureType.COVER_FRONT:
|
||||
cover_info = dat
|
||||
break
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||
cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||
tmp_cover_name = cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||
elif original_file_extension in [".aac"]:
|
||||
title = audio_file.tags.get('Title').value if "Title" in audio_file else None
|
||||
author = audio_file.tags.get('Artist').value if "Artist" in audio_file else None
|
||||
@@ -92,7 +87,7 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na
|
||||
pubdate = audio_file.tags.get('Year').value if "Year" in audio_file else None
|
||||
cover_data = audio_file.tags['Cover Art (Front)']
|
||||
if cover_data and not no_cover_processing:
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||
tmp_cover_name = tmp_file_path + '.jpg'
|
||||
with open(tmp_cover_name, "wb") as cover_file:
|
||||
cover_file.write(cover_data.value.split(b"\x00",1)[1])
|
||||
elif original_file_extension in [".asf"]:
|
||||
@@ -106,7 +101,7 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na
|
||||
pubdate = audio_file.tags.get('Year')[0].value if "Year" in audio_file else None
|
||||
cover_data = audio_file.tags.get('WM/Picture', None)
|
||||
if cover_data and not no_cover_processing:
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||
tmp_cover_name = tmp_file_path + '.jpg'
|
||||
with open(tmp_cover_name, "wb") as cover_file:
|
||||
cover_file.write(cover_data[0].value)
|
||||
elif original_file_extension in [".mp4", ".m4a", ".m4b"]:
|
||||
@@ -120,7 +115,6 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na
|
||||
pubdate = audio_file.tags.get('©day')[0] if "©day" in audio_file.tags else None
|
||||
cover_data = audio_file.tags.get('covr', None)
|
||||
if cover_data and not no_cover_processing:
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||
cover_type = None
|
||||
for c in cover_data:
|
||||
if c.imageformat == mutagen.mp4.AtomDataType.JPEG:
|
||||
@@ -132,7 +126,7 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na
|
||||
cover_bin = c
|
||||
break
|
||||
if cover_type:
|
||||
cover.cover_processing(tmp_file_path, cover_bin, cover_type)
|
||||
tmp_cover_name = cover.cover_processing(tmp_file_path, cover_bin, cover_type)
|
||||
else:
|
||||
logger.error("Unknown covertype in file {} ".format(original_file_name))
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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, carderne
|
||||
#
|
||||
# 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 cps.pagination import Pagination
|
||||
from flask import Blueprint
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import get_locale
|
||||
from flask import request, redirect, url_for
|
||||
|
||||
from . import logger, isoLanguages
|
||||
from . import db, config
|
||||
from . import calibre_db
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
from .render_template import render_title_template
|
||||
from .web import get_sort_function
|
||||
|
||||
try:
|
||||
from natsort import natsorted as sort
|
||||
except ImportError:
|
||||
sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files
|
||||
|
||||
basic = Blueprint('basic', __name__)
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
@basic.route("/basic", methods=["GET"])
|
||||
@login_required_if_no_ano
|
||||
def index():
|
||||
term = request.args.get("query", "") # default to showing all books
|
||||
limit = 15
|
||||
page = int(request.args.get("page") or 1)
|
||||
off = (page - 1) * limit
|
||||
order = get_sort_function("stored", "search")
|
||||
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
|
||||
entries, result_count, pagination = calibre_db.get_search_results(term,
|
||||
config,
|
||||
off,
|
||||
order,
|
||||
limit,
|
||||
*join)
|
||||
return render_title_template('basic_index.html',
|
||||
searchterm=term,
|
||||
pagination=pagination,
|
||||
query=term,
|
||||
adv_searchterm=term,
|
||||
entries=entries,
|
||||
result_count=result_count,
|
||||
title=_("Search"),
|
||||
page="search",
|
||||
order=order[1])
|
||||
|
||||
|
||||
@basic.route("/basic_book/<int:book_id>")
|
||||
@login_required_if_no_ano
|
||||
def show_book(book_id):
|
||||
entries = calibre_db.get_book_read_archived(book_id, config.config_read_column, allow_show_archived=True)
|
||||
if entries:
|
||||
entry = entries[0]
|
||||
for lang_index in range(0, len(entry.languages)):
|
||||
entry.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[
|
||||
lang_index].lang_code)
|
||||
entry.ordered_authors = calibre_db.order_authors([entry])
|
||||
|
||||
return render_title_template('basic_detail.html',
|
||||
entry=entry,
|
||||
is_xhr=request.headers.get('X-Requested-With') == 'XMLHttpRequest',
|
||||
title=entry.title,
|
||||
page="book")
|
||||
else:
|
||||
log.debug("Selected book is unavailable. File does not exist or is not accessible")
|
||||
return redirect(url_for("basic.index"))
|
||||
+6
-6
@@ -90,7 +90,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec
|
||||
if len(ext) > 1:
|
||||
extension = ext[1].lower()
|
||||
if extension in cover.COVER_EXTENSIONS:
|
||||
cover_data = cf.read([name])
|
||||
cover_data = cf.read(name)
|
||||
break
|
||||
except Exception as ex:
|
||||
log.error('Rarfile failed with error: {}'.format(ex))
|
||||
@@ -109,13 +109,13 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec
|
||||
return cover_data, extension
|
||||
|
||||
|
||||
def _extract_cover(tmp_file_name, original_file_extension, rar_executable):
|
||||
def _extract_cover(tmp_file_path, original_file_extension, rar_executable):
|
||||
cover_data = extension = None
|
||||
if use_comic_meta:
|
||||
try:
|
||||
archive = ComicArchive(tmp_file_name, rar_exe_path=rar_executable)
|
||||
archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable)
|
||||
except TypeError:
|
||||
archive = ComicArchive(tmp_file_name)
|
||||
archive = ComicArchive(tmp_file_path)
|
||||
name_list = archive.getPageNameList if hasattr(archive, "getPageNameList") else archive.get_page_name_list
|
||||
for index, name in enumerate(name_list()):
|
||||
ext = os.path.splitext(name)
|
||||
@@ -126,8 +126,8 @@ def _extract_cover(tmp_file_name, original_file_extension, rar_executable):
|
||||
cover_data = get_page(index)
|
||||
break
|
||||
else:
|
||||
cover_data, extension = _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_executable)
|
||||
return cover.cover_processing(tmp_file_name, cover_data, extension)
|
||||
cover_data, extension = _extract_cover_from_archive(original_file_extension, tmp_file_path, rar_executable)
|
||||
return cover.cover_processing(tmp_file_path, cover_data, extension)
|
||||
|
||||
|
||||
def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rar_executable, no_cover_processing):
|
||||
|
||||
+12
-4
@@ -405,11 +405,13 @@ class ConfigSQL(object):
|
||||
return self.config_calibre_split_dir if self.config_calibre_split_dir else self.config_calibre_dir
|
||||
|
||||
def store_calibre_uuid(self, calibre_db, Library_table):
|
||||
from . import app
|
||||
try:
|
||||
calibre_uuid = calibre_db.session.query(Library_table).one_or_none()
|
||||
if self.config_calibre_uuid != calibre_uuid.uuid:
|
||||
self.config_calibre_uuid = calibre_uuid.uuid
|
||||
self.save()
|
||||
with app.app_context():
|
||||
calibre_uuid = calibre_db.session.query(Library_table).one_or_none()
|
||||
if self.config_calibre_uuid != calibre_uuid.uuid:
|
||||
self.config_calibre_uuid = calibre_uuid.uuid
|
||||
self.save()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@@ -483,6 +485,8 @@ def autodetect_calibre_binaries():
|
||||
"C:\\program files(x86)\\calibre\\",
|
||||
"C:\\program files(x86)\\calibre2\\",
|
||||
"C:\\program files\\calibre2\\"]
|
||||
elif sys.platform.startswith("freebsd"):
|
||||
calibre_path = ["/usr/local/bin/"]
|
||||
else:
|
||||
calibre_path = ["/opt/calibre/"]
|
||||
for element in calibre_path:
|
||||
@@ -513,6 +517,8 @@ def autodetect_unrar_binary():
|
||||
if sys.platform == "win32":
|
||||
calibre_path = ["C:\\program files\\WinRar\\unRAR.exe",
|
||||
"C:\\program files(x86)\\WinRar\\unRAR.exe"]
|
||||
elif sys.platform.startswith("freebsd"):
|
||||
calibre_path = ["/usr/local/bin/unrar"]
|
||||
else:
|
||||
calibre_path = ["/usr/bin/unrar"]
|
||||
for element in calibre_path:
|
||||
@@ -525,6 +531,8 @@ def autodetect_kepubify_binary():
|
||||
if sys.platform == "win32":
|
||||
calibre_path = ["C:\\program files\\kepubify\\kepubify-windows-64Bit.exe",
|
||||
"C:\\program files(x86)\\kepubify\\kepubify-windows-64Bit.exe"]
|
||||
elif sys.platform.startswith("freebsd"):
|
||||
calibre_path = ["/usr/local/bin/kepubify"]
|
||||
else:
|
||||
calibre_path = ["/opt/kepubify/kepubify-linux-64bit", "/opt/kepubify/kepubify-linux-32bit"]
|
||||
for element in calibre_path:
|
||||
|
||||
+1
-1
@@ -172,7 +172,7 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d
|
||||
'series_id, languages, publisher, pubdate, identifiers')
|
||||
|
||||
# python build process likes to have x.y.zbw -> b for beta and w a counting number
|
||||
STABLE_VERSION = '0.6.24'
|
||||
STABLE_VERSION = '0.6.25'
|
||||
|
||||
NIGHTLY_VERSION = dict()
|
||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||
|
||||
+4
-3
@@ -29,13 +29,14 @@ NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp']
|
||||
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')
|
||||
def cover_processing(tmp_file_path, img, extension):
|
||||
# tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
|
||||
tmp_cover_name = tmp_file_path + '.jpg'
|
||||
if extension in NO_JPEG_EXTENSIONS:
|
||||
if use_IM:
|
||||
with Image(blob=img) as imgc:
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace('rgb')
|
||||
imgc.transform_colorspace('srgb')
|
||||
imgc.save(filename=tmp_cover_name)
|
||||
return tmp_cover_name
|
||||
else:
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ def get_user_locale_language(user_language):
|
||||
|
||||
|
||||
def get_available_locale():
|
||||
return [Locale('en')] + babel.list_translations()
|
||||
return sorted(babel.list_translations(), key=lambda x: x.display_name.lower())
|
||||
|
||||
|
||||
def get_available_translations():
|
||||
|
||||
@@ -23,7 +23,7 @@ import json
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote
|
||||
import unidecode
|
||||
from weakref import WeakSet
|
||||
# from weakref import WeakSet
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlite3 import OperationalError as sqliteOperationalError
|
||||
@@ -45,7 +45,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from .cw_login import current_user
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import get_locale
|
||||
from flask import flash
|
||||
from flask import flash, g, Flask
|
||||
|
||||
from . import logger, ub, isoLanguages
|
||||
from .pagination import Pagination
|
||||
@@ -102,6 +102,16 @@ class Identifiers(Base):
|
||||
type = Column(String(collation='NOCASE'), nullable=False, default="isbn")
|
||||
val = Column(String(collation='NOCASE'), nullable=False)
|
||||
book = Column(Integer, ForeignKey('books.id'), nullable=False)
|
||||
amazon = {
|
||||
"jp": "co.jp",
|
||||
"uk": "co.uk",
|
||||
"us": "com",
|
||||
"au": "com.au",
|
||||
"be": "com.be",
|
||||
"br": "com.br",
|
||||
"tr": "com.tr",
|
||||
"mx": "com.mx",
|
||||
}
|
||||
|
||||
def __init__(self, val, id_type, book):
|
||||
super().__init__()
|
||||
@@ -114,7 +124,11 @@ class Identifiers(Base):
|
||||
if format_type == 'amazon':
|
||||
return "Amazon"
|
||||
elif format_type.startswith("amazon_"):
|
||||
return "Amazon.{0}".format(format_type[7:].lower().replace("uk","co.uk"))
|
||||
label_amazon = "Amazon.{0}"
|
||||
country_code = format_type[7:].lower()
|
||||
if country_code not in self.amazon:
|
||||
return label_amazon.format(country_code)
|
||||
return label_amazon.format(self.amazon[country_code])
|
||||
elif format_type == "isbn":
|
||||
return "ISBN"
|
||||
elif format_type == "doi":
|
||||
@@ -137,6 +151,12 @@ class Identifiers(Base):
|
||||
return "ISSN"
|
||||
elif format_type == "isfdb":
|
||||
return "ISFDB"
|
||||
elif format_type == "storygraph":
|
||||
return "StoryGraph"
|
||||
elif format_type == "ebooks":
|
||||
return "eBooks.com"
|
||||
elif format_type == "smashwords":
|
||||
return "Smashwords"
|
||||
if format_type == "lubimyczytac":
|
||||
return "Lubimyczytac"
|
||||
if format_type == "databazeknih":
|
||||
@@ -149,7 +169,11 @@ class Identifiers(Base):
|
||||
if format_type == "amazon" or format_type == "asin":
|
||||
return "https://amazon.com/dp/{0}".format(self.val)
|
||||
elif format_type.startswith('amazon_'):
|
||||
return "https://amazon.{0}/dp/{1}".format(format_type[7:].lower().replace("uk","co.uk"), self.val)
|
||||
link_amazon = "https://amazon.{0}/dp/{1}"
|
||||
country_code = format_type[7:].lower()
|
||||
if country_code not in self.amazon:
|
||||
return link_amazon.format(country_code, self.val)
|
||||
return link_amazon.format(self.amazon[country_code], self.val)
|
||||
elif format_type == "isbn":
|
||||
return "https://www.worldcat.org/isbn/{0}".format(self.val)
|
||||
elif format_type == "doi":
|
||||
@@ -173,9 +197,15 @@ class Identifiers(Base):
|
||||
elif format_type == "issn":
|
||||
return "https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
||||
elif format_type == "isfdb":
|
||||
return "http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
||||
return "https://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
||||
elif format_type == "databazeknih":
|
||||
return "https://www.databazeknih.cz/knihy/{0}".format(self.val)
|
||||
elif format_type == "storygraph":
|
||||
return "https://app.thestorygraph.com/books/{0}".format(self.val)
|
||||
elif format_type == "ebooks":
|
||||
return "https://www.ebooks.com/en-us/book/{0}".format(self.val)
|
||||
elif format_type == "smashwords":
|
||||
return "https://www.smashwords.com/books/view/{0}".format(self.val)
|
||||
elif self.val.lower().startswith("javascript:"):
|
||||
return quote(self.val)
|
||||
elif self.val.lower().startswith("data:"):
|
||||
@@ -510,34 +540,25 @@ class AlchemyEncoder(json.JSONEncoder):
|
||||
|
||||
|
||||
class CalibreDB:
|
||||
_init = False
|
||||
engine = None
|
||||
config = None
|
||||
session_factory = None
|
||||
# This is a WeakSet so that references here don't keep other CalibreDB
|
||||
# instances alive once they reach the end of their respective scopes
|
||||
instances = WeakSet()
|
||||
config_calibre_dir = None
|
||||
app_db_path = None
|
||||
|
||||
def __init__(self, expire_on_commit=True, init=False):
|
||||
def __init__(self, _app: Flask=None): # , expire_on_commit=True, init=False):
|
||||
""" Initialize a new CalibreDB session
|
||||
"""
|
||||
self.session = None
|
||||
if init:
|
||||
self.init_db(expire_on_commit)
|
||||
self.Session = None
|
||||
#if init:
|
||||
# self.init_db(expire_on_commit)
|
||||
if _app is not None and not _app._got_first_request:
|
||||
self.init_app(_app)
|
||||
|
||||
def init_db(self, expire_on_commit=True):
|
||||
if self._init:
|
||||
self.init_session(expire_on_commit)
|
||||
|
||||
self.instances.add(self)
|
||||
|
||||
def init_session(self, expire_on_commit=True):
|
||||
self.session = self.session_factory()
|
||||
self.session.expire_on_commit = expire_on_commit
|
||||
self.create_functions(self.config)
|
||||
def init_app(self, _app):
|
||||
_app.teardown_appcontext(self.teardown)
|
||||
|
||||
@classmethod
|
||||
def setup_db_cc_classes(cls, cc):
|
||||
global cc_classes
|
||||
cc_ids = []
|
||||
books_custom_column_links = {}
|
||||
for row in cc:
|
||||
@@ -605,8 +626,6 @@ class CalibreDB:
|
||||
secondary=books_custom_column_links[cc_id[0]],
|
||||
backref='books'))
|
||||
|
||||
return cc_classes
|
||||
|
||||
@classmethod
|
||||
def check_valid_db(cls, config_calibre_dir, app_db_path, config_calibre_uuid):
|
||||
if not config_calibre_dir:
|
||||
@@ -626,7 +645,6 @@ class CalibreDB:
|
||||
local_session = scoped_session(sessionmaker())
|
||||
local_session.configure(bind=connection)
|
||||
database_uuid = local_session().query(Library_Id).one_or_none()
|
||||
# local_session.dispose()
|
||||
|
||||
check_engine.connect()
|
||||
db_change = config_calibre_uuid != database_uuid.uuid
|
||||
@@ -634,13 +652,30 @@ class CalibreDB:
|
||||
return False, False
|
||||
return True, db_change
|
||||
|
||||
def teardown(self, exception):
|
||||
ctx = g.get("lib_sql")
|
||||
if ctx:
|
||||
ctx.close()
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
# connect or get active connection
|
||||
if not g.get("lib_sql"):
|
||||
g.lib_sql = self.connect()
|
||||
return g.lib_sql
|
||||
|
||||
@classmethod
|
||||
def update_config(cls, config):
|
||||
def update_config(cls, config, config_calibre_dir, app_db_path):
|
||||
cls.config = config
|
||||
cls.config_calibre_dir = config_calibre_dir
|
||||
cls.app_db_path = app_db_path
|
||||
|
||||
|
||||
def connect(self):
|
||||
return self.setup_db(self.config_calibre_dir, self.app_db_path)
|
||||
|
||||
@classmethod
|
||||
def setup_db(cls, config_calibre_dir, app_db_path):
|
||||
cls.dispose()
|
||||
|
||||
if not config_calibre_dir:
|
||||
cls.config.invalidate()
|
||||
@@ -652,16 +687,17 @@ class CalibreDB:
|
||||
return None
|
||||
|
||||
try:
|
||||
cls.engine = create_engine('sqlite://',
|
||||
engine = create_engine('sqlite://',
|
||||
echo=False,
|
||||
isolation_level="SERIALIZABLE",
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=StaticPool)
|
||||
with cls.engine.begin() as connection:
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text('PRAGMA cache_size = 10000;'))
|
||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
||||
|
||||
conn = cls.engine.connect()
|
||||
conn = engine.connect()
|
||||
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
||||
except Exception as ex:
|
||||
cls.config.invalidate(ex)
|
||||
@@ -677,13 +713,10 @@ class CalibreDB:
|
||||
log.error_or_exception(e)
|
||||
return None
|
||||
|
||||
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
|
||||
autoflush=True,
|
||||
bind=cls.engine, future=True))
|
||||
for inst in cls.instances:
|
||||
inst.init_session()
|
||||
return scoped_session(sessionmaker(autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine, future=True))
|
||||
|
||||
cls._init = True
|
||||
|
||||
def get_book(self, book_id):
|
||||
return self.session.query(Books).filter(Books.id == book_id).first()
|
||||
@@ -880,7 +913,8 @@ class CalibreDB:
|
||||
results = self.session.query(Authors).filter(Authors.sort == auth).all()
|
||||
# ToDo: How to handle not found author name
|
||||
if not len(results):
|
||||
log.error("Author {} not found to display name in right order".format(auth))
|
||||
book_id = entry.id if isinstance(entry, Books) else entry[0].id
|
||||
log.error("Author '{}' of book {} not found to display name in right order".format(auth, book_id))
|
||||
# error = True
|
||||
break
|
||||
for r in results:
|
||||
@@ -1047,43 +1081,11 @@ class CalibreDB:
|
||||
except sqliteOperationalError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def dispose(cls):
|
||||
# global session
|
||||
|
||||
for inst in cls.instances:
|
||||
old_session = inst.session
|
||||
inst.session = None
|
||||
if old_session:
|
||||
try:
|
||||
old_session.close()
|
||||
except Exception:
|
||||
pass
|
||||
if old_session.bind:
|
||||
try:
|
||||
old_session.bind.dispose()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for attr in list(Books.__dict__.keys()):
|
||||
if attr.startswith("custom_column_"):
|
||||
setattr(Books, attr, None)
|
||||
|
||||
for db_class in cc_classes.values():
|
||||
Base.metadata.remove(db_class.__table__)
|
||||
cc_classes.clear()
|
||||
|
||||
for table in reversed(Base.metadata.sorted_tables):
|
||||
name = table.key
|
||||
if name.startswith("custom_column_") or name.startswith("books_custom_column_"):
|
||||
if table is not None:
|
||||
Base.metadata.remove(table)
|
||||
|
||||
def reconnect_db(self, config, app_db_path):
|
||||
self.dispose()
|
||||
self.engine.dispose()
|
||||
# self.dispose()
|
||||
# self.engine.dispose()
|
||||
self.setup_db(config.config_calibre_dir, app_db_path)
|
||||
self.update_config(config)
|
||||
self.update_config(config, config.config_calibre_dir, app_db_path)
|
||||
|
||||
|
||||
def lcase(s):
|
||||
|
||||
+3
-4
@@ -23,11 +23,10 @@ import zipfile
|
||||
import json
|
||||
from io import BytesIO
|
||||
from flask_babel.speaklater import LazyString
|
||||
|
||||
from importlib.metadata import metadata
|
||||
import os
|
||||
|
||||
from flask import send_file
|
||||
import importlib
|
||||
|
||||
from . import logger, config
|
||||
from .about import collect_stats
|
||||
@@ -50,7 +49,7 @@ def assemble_logfiles(file_name):
|
||||
with open(f, 'rb') as fd:
|
||||
shutil.copyfileobj(fd, wfd)
|
||||
wfd.seek(0)
|
||||
version = importlib.metadata.version("flask")
|
||||
version = metadata("flask")["Version"]
|
||||
if int(version.split('.')[0]) < 2:
|
||||
return send_file(wfd,
|
||||
as_attachment=True,
|
||||
@@ -74,7 +73,7 @@ def send_debug():
|
||||
for fp in file_list:
|
||||
zf.write(fp, os.path.basename(fp))
|
||||
memory_zip.seek(0)
|
||||
version = importlib.metadata.version("flask")
|
||||
version = metadata("flask")["Version"]
|
||||
if int(version.split('.')[0]) < 2:
|
||||
return send_file(memory_zip,
|
||||
as_attachment=True,
|
||||
|
||||
+14
-14
@@ -43,23 +43,23 @@ def load_dependencies(optional=False):
|
||||
r'(?:(python_version)\s?([<=>]+)\s?\'([\d\.]+)\'|'
|
||||
r'(sys_platform)\s?([\!=]+)\s?\'([\w]+)\'))?', line.strip())
|
||||
try:
|
||||
if res.group(7) and res.group(8):
|
||||
val = res.group(8).split(".")
|
||||
if not eval(str(sys.version_info[0]) + "." + "{:02d}".format(sys.version_info[1]) +
|
||||
res.group(7) + val[0] + "." + "{:02d}".format(int(val[1]))):
|
||||
continue
|
||||
elif res.group(10) and res.group(11):
|
||||
# only installed if platform is eqal, don't check if platform is not equal
|
||||
if res.group(10) == "==":
|
||||
if sys.platform != res.group(11):
|
||||
continue
|
||||
# installed if platform is not eqal, don't check if platform is equal
|
||||
elif res.group(10) == "!=":
|
||||
if sys.platform == res.group(11):
|
||||
continue
|
||||
if getattr(sys, 'frozen', False):
|
||||
dep_version = exe_deps[res.group(1).lower().replace('_', '-')]
|
||||
else:
|
||||
if res.group(7) and res.group(8):
|
||||
val = res.group(8).split(".")
|
||||
if not eval(str(sys.version_info[0]) + "." + "{:02d}".format(sys.version_info[1]) +
|
||||
res.group(7) + val[0] + "." + "{:02d}".format(int(val[1]))):
|
||||
continue
|
||||
elif res.group(10) and res.group(11):
|
||||
# only installed if platform is eqal, don't check if platform is not equal
|
||||
if res.group(10) == "==":
|
||||
if sys.platform != res.group(11):
|
||||
continue
|
||||
# installed if platform is not eqal, don't check if platform is equal
|
||||
elif res.group(10) == "!=":
|
||||
if sys.platform == res.group(11):
|
||||
continue
|
||||
if importlib:
|
||||
dep_version = version(res.group(1))
|
||||
else:
|
||||
|
||||
+46
-61
@@ -28,11 +28,11 @@ from shutil import copyfile
|
||||
from markupsafe import escape, Markup # dependency of flask
|
||||
from functools import wraps
|
||||
|
||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Response
|
||||
from flask import Blueprint, request, flash, redirect, url_for, abort, jsonify, make_response, Response
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import lazy_gettext as N_
|
||||
from flask_babel import get_locale
|
||||
from .cw_login import current_user, login_required
|
||||
from .cw_login import current_user
|
||||
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
|
||||
from sqlalchemy.orm.exc import StaleDataError
|
||||
from sqlalchemy.sql.expression import func
|
||||
@@ -76,7 +76,7 @@ def edit_required(f):
|
||||
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
||||
@user_login_required
|
||||
def delete_book_from_details(book_id):
|
||||
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
|
||||
return delete_book_from_table(book_id, "", True)
|
||||
|
||||
|
||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
||||
@@ -158,16 +158,16 @@ def upload():
|
||||
if len(request.files.getlist("btn-upload")) < 2:
|
||||
if current_user.role_edit() or current_user.role_admin():
|
||||
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
|
||||
return Response(json.dumps(resp), mimetype='application/json')
|
||||
return make_response(jsonify(resp))
|
||||
else:
|
||||
resp = {"location": url_for('web.show_book', book_id=book_id)}
|
||||
return Response(json.dumps(resp), mimetype='application/json')
|
||||
return make_response(jsonify(resp))
|
||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||
calibre_db.session.rollback()
|
||||
log.error_or_exception("Database error: {}".format(e))
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
|
||||
category="error")
|
||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||
return make_response(jsonify(location=url_for("web.index")))
|
||||
abort(404)
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ def table_get_custom_enum(c_id):
|
||||
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)
|
||||
return make_response(jsonify(ret))
|
||||
|
||||
|
||||
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
||||
@@ -215,73 +215,60 @@ def table_get_custom_enum(c_id):
|
||||
def edit_list_book(param):
|
||||
vals = request.form.to_dict()
|
||||
book = calibre_db.get_book(vals['pk'])
|
||||
calibre_db.create_functions(config)
|
||||
sort_param = ""
|
||||
ret = ""
|
||||
try:
|
||||
if param == 'series_index':
|
||||
edit_book_series_index(vals['value'], book)
|
||||
ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True, newValue=book.series_index))
|
||||
elif param == 'tags':
|
||||
edit_book_tags(vals['value'], book)
|
||||
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags])))
|
||||
elif param == 'series':
|
||||
edit_book_series(vals['value'], book)
|
||||
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True, newValue=', '.join([serie.name for serie in book.series])))
|
||||
elif param == 'publishers':
|
||||
edit_book_publisher(vals['value'], book)
|
||||
ret = Response(json.dumps({'success': True,
|
||||
'newValue': ', '.join([publisher.name for publisher in book.publishers])}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True,
|
||||
newValue=', '.join([publisher.name for publisher in book.publishers])))
|
||||
elif param == 'languages':
|
||||
invalid = list()
|
||||
edit_book_languages(vals['value'], book, invalid=invalid)
|
||||
if invalid:
|
||||
ret = Response(json.dumps({'success': False,
|
||||
'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=False,
|
||||
msg='Invalid languages in request: {}'.format(','.join(invalid))))
|
||||
else:
|
||||
lang_names = list()
|
||||
for lang in book.languages:
|
||||
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
|
||||
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True, newValue=', '.join(lang_names)))
|
||||
elif param == 'author_sort':
|
||||
book.author_sort = vals['value']
|
||||
ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True, newValue=book.author_sort))
|
||||
elif param == 'title':
|
||||
sort_param = book.sort
|
||||
if handle_title_on_edit(book, vals.get('value', "")):
|
||||
rename_error = helper.update_dir_structure(book.id, config.get_book_path())
|
||||
if not rename_error:
|
||||
ret = Response(json.dumps({'success': True, 'newValue': book.title}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True, newValue=book.title))
|
||||
else:
|
||||
ret = Response(json.dumps({'success': False,
|
||||
'msg': rename_error}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=False, msg=rename_error))
|
||||
elif param == 'sort':
|
||||
book.sort = vals['value']
|
||||
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True,newValue=book.sort))
|
||||
elif param == 'comments':
|
||||
edit_book_comments(vals['value'], book)
|
||||
ret = Response(json.dumps({'success': True, 'newValue': book.comments[0].text}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True, newValue=book.comments[0].text))
|
||||
elif param == 'authors':
|
||||
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
|
||||
rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0])
|
||||
if not rename_error:
|
||||
ret = Response(json.dumps({
|
||||
'success': True,
|
||||
'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(
|
||||
success=True,
|
||||
newValue=' & '.join([author.replace('|', ',') for author in input_authors])))
|
||||
else:
|
||||
ret = Response(json.dumps({'success': False,
|
||||
'msg': rename_error}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=False, msg=rename_error))
|
||||
elif param == 'is_archived':
|
||||
is_archived = change_archived_books(book.id, vals['value'] == "True",
|
||||
message="Book {} archive bit set to: {}".format(book.id, vals['value']))
|
||||
@@ -300,8 +287,7 @@ def edit_list_book(param):
|
||||
if vals['value'] in ["True", "False"]:
|
||||
ret = ""
|
||||
else:
|
||||
ret = Response(json.dumps({'success': True, 'newValue': vals['value']}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=True, newValue=vals['value']))
|
||||
else:
|
||||
return _("Parameter not found"), 400
|
||||
book.last_modified = datetime.now(timezone.utc)
|
||||
@@ -314,9 +300,8 @@ def edit_list_book(param):
|
||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||
calibre_db.session.rollback()
|
||||
log.error_or_exception("Database error: {}".format(e))
|
||||
ret = Response(json.dumps({'success': False,
|
||||
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}),
|
||||
mimetype='application/json')
|
||||
ret = make_response(jsonify(success=False,
|
||||
msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e)))
|
||||
return ret
|
||||
|
||||
|
||||
@@ -327,13 +312,13 @@ def get_sorted_entry(field, bookid):
|
||||
book = calibre_db.get_filtered_book(bookid)
|
||||
if book:
|
||||
if field == 'title':
|
||||
return json.dumps({'sort': book.sort})
|
||||
return make_response(jsonify(sort=book.sort))
|
||||
elif field == 'authors':
|
||||
return json.dumps({'author_sort': book.author_sort})
|
||||
return make_response(jsonify(author_sort=book.author_sort))
|
||||
if field == 'sort':
|
||||
return json.dumps({'sort': book.title})
|
||||
return make_response(jsonify(sort=book.title))
|
||||
if field == 'author_sort':
|
||||
return json.dumps({'authors': " & ".join([a.name for a in calibre_db.order_authors([book])])})
|
||||
return make_response(jsonify(authors=" & ".join([a.name for a in calibre_db.order_authors([book])])))
|
||||
return ""
|
||||
|
||||
|
||||
@@ -349,7 +334,7 @@ def simulate_merge_list_book():
|
||||
from_book = []
|
||||
for book_id in vals:
|
||||
from_book.append(calibre_db.get_book(book_id).title)
|
||||
return json.dumps({'to': to_book, 'from': from_book})
|
||||
return make_response(jsonify({'to': to_book, 'from': from_book}))
|
||||
return ""
|
||||
|
||||
|
||||
@@ -387,7 +372,7 @@ def merge_list_book():
|
||||
element.uncompressed_size,
|
||||
to_name))
|
||||
delete_book_from_table(from_book.id, "", True)
|
||||
return json.dumps({'success': True})
|
||||
return make_response(jsonify(success=True))
|
||||
return ""
|
||||
|
||||
|
||||
@@ -427,11 +412,11 @@ def table_xchange_author_title():
|
||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||
calibre_db.session.rollback()
|
||||
log.error_or_exception("Database error: {}".format(e))
|
||||
return json.dumps({'success': False})
|
||||
return make_response(jsonify(success=False))
|
||||
|
||||
if config.config_use_google_drive:
|
||||
gdriveutils.updateGdriveCalibreFromLocal()
|
||||
return json.dumps({'success': True})
|
||||
return make_response(jsonify(success=True))
|
||||
return ""
|
||||
|
||||
|
||||
@@ -559,7 +544,7 @@ def do_edit_book(book_id, upload_formats=None):
|
||||
|
||||
if upload_formats:
|
||||
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
|
||||
return Response(json.dumps(resp), mimetype='application/json')
|
||||
return make_response(jsonify(resp))
|
||||
|
||||
if "detail_view" in to_save:
|
||||
return redirect(url_for('web.show_book', book_id=book.id))
|
||||
@@ -768,17 +753,17 @@ def file_handling_on_upload(requested_file):
|
||||
if config.config_check_extensions and allowed_extensions != ['']:
|
||||
if not validate_mime_type(requested_file, allowed_extensions):
|
||||
flash(_("File type isn't allowed to be uploaded to this server"), category="error")
|
||||
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||
return None, make_response(jsonify(location=url_for("web.index")))
|
||||
if '.' in requested_file.filename:
|
||||
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
||||
if file_ext not in allowed_extensions and '' not in allowed_extensions:
|
||||
flash(
|
||||
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
|
||||
ext=file_ext), category="error")
|
||||
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||
return None, make_response(jsonify(location=url_for("web.index")))
|
||||
else:
|
||||
flash(_('File to be uploaded must have an extension'), category="error")
|
||||
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||
return None, make_response(jsonify(location=url_for("web.index")))
|
||||
|
||||
# extract metadata from file
|
||||
try:
|
||||
@@ -787,7 +772,7 @@ def file_handling_on_upload(requested_file):
|
||||
log.error("File %s could not saved to temp dir", requested_file.filename)
|
||||
flash(_("File %(filename)s could not saved to temp dir",
|
||||
filename=requested_file.filename), category="error")
|
||||
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||
return None, make_response(jsonify(location=url_for("web.index")))
|
||||
return meta, None
|
||||
|
||||
|
||||
@@ -859,7 +844,7 @@ def delete_whole_book(book_id, book):
|
||||
def render_delete_book_result(book_format, json_response, warning, book_id, location=""):
|
||||
if book_format:
|
||||
if json_response:
|
||||
return json.dumps([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||
return jsonify([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||
"type": "success",
|
||||
"format": book_format,
|
||||
"message": _('Book Format Successfully Deleted')}])
|
||||
@@ -868,7 +853,7 @@ def render_delete_book_result(book_format, json_response, warning, book_id, loca
|
||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||
else:
|
||||
if json_response:
|
||||
return json.dumps([warning, {"location": get_redirect_location(location, "web.index"),
|
||||
return jsonify([warning, {"location": get_redirect_location(location, "web.index"),
|
||||
"type": "success",
|
||||
"format": book_format,
|
||||
"message": _('Book Successfully Deleted')}])
|
||||
@@ -886,7 +871,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
|
||||
result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
|
||||
if not result:
|
||||
if json_response:
|
||||
return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||
"type": "danger",
|
||||
"format": "",
|
||||
"message": error}])
|
||||
@@ -913,7 +898,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
|
||||
log.error_or_exception(ex)
|
||||
calibre_db.session.rollback()
|
||||
if json_response:
|
||||
return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||
"type": "danger",
|
||||
"format": "",
|
||||
"message": ex}])
|
||||
@@ -927,7 +912,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
|
||||
return render_delete_book_result(book_format, json_response, warning, book_id, location)
|
||||
message = _("You are missing permissions to delete books")
|
||||
if json_response:
|
||||
return json.dumps({"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||
return jsonify({"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||
"type": "danger",
|
||||
"format": "",
|
||||
"message": message})
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ def get_epub_layout(book, book_data):
|
||||
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=default_ns)[0]
|
||||
|
||||
layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=default_ns)
|
||||
except (etree.XMLSyntaxError, KeyError, IndexError, OSError) as e:
|
||||
except (etree.XMLSyntaxError, KeyError, IndexError, OSError, UnicodeDecodeError) as e:
|
||||
log.error("Could not parse epub metadata of book {} during kobo sync: {}".format(book.id, e))
|
||||
layout = []
|
||||
|
||||
|
||||
@@ -37,17 +37,31 @@ def error_http(error):
|
||||
error_code="Error {0}".format(error.code),
|
||||
error_name=error.name,
|
||||
issue=False,
|
||||
goto_admin=False,
|
||||
unconfigured=not config.db_configured,
|
||||
instance=config.config_calibre_web_title
|
||||
), error.code
|
||||
|
||||
|
||||
def internal_error(error):
|
||||
if (isinstance(error.original_exception, AttributeError) and
|
||||
error.original_exception.args[0] == "'NoneType' object has no attribute 'query'"
|
||||
and error.original_exception.name == "query"):
|
||||
return render_template('http_error.html',
|
||||
error_code="Database Error",
|
||||
error_name='The library used is invalid or has permission errors',
|
||||
issue=False,
|
||||
goto_admin=True,
|
||||
unconfigured=False,
|
||||
error_stack="",
|
||||
instance=config.config_calibre_web_title
|
||||
), 500
|
||||
return render_template('http_error.html',
|
||||
error_code="500 Internal Server Error",
|
||||
error_name='The server encountered an internal error and was unable to complete your '
|
||||
'request. There is an error in the application.',
|
||||
issue=True,
|
||||
goto_admin=False,
|
||||
unconfigured=False,
|
||||
error_stack=traceback.format_exc().split("\n"),
|
||||
instance=config.config_calibre_web_title
|
||||
|
||||
+10
-1
@@ -34,6 +34,15 @@ except ImportError as e:
|
||||
error = "Cannot import python-magic, checking uploaded file metadata will not work: {}".format(e)
|
||||
|
||||
|
||||
def get_mimetype(ext):
|
||||
# overwrite some mimetypes for proper file detection
|
||||
mimes = {".fb2": "text/xml",
|
||||
".cbz": "application/zip",
|
||||
".cbr": "application/x-rar"
|
||||
}
|
||||
return mimes.get(ext, mimetypes.types_map[ext])
|
||||
|
||||
|
||||
def get_temp_dir():
|
||||
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
|
||||
if not os.path.isdir(tmp_dir):
|
||||
@@ -54,7 +63,7 @@ def validate_mime_type(file_buffer, allowed_extensions):
|
||||
allowed_mimetypes = list()
|
||||
for x in allowed_extensions:
|
||||
try:
|
||||
allowed_mimetypes.append(mimetypes.types_map["." + x])
|
||||
allowed_mimetypes.append(get_mimetype("." + x))
|
||||
except KeyError:
|
||||
log.error("Unkown mimetype for Extension: {}".format(x))
|
||||
tmp_mime_type = mime.from_buffer(file_buffer.read())
|
||||
|
||||
+14
-10
@@ -30,7 +30,7 @@ import requests
|
||||
import unidecode
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import send_from_directory, make_response, abort, url_for, Response
|
||||
from flask import send_from_directory, make_response, abort, url_for, Response, request
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import lazy_gettext as N_
|
||||
from flask_babel import get_locale
|
||||
@@ -199,7 +199,7 @@ def check_send_to_ereader(entry):
|
||||
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
|
||||
# list with supported formats
|
||||
def check_read_formats(entry):
|
||||
extensions_reader = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU', 'DJV'}
|
||||
extensions_reader = {'TXT', 'PDF', 'EPUB', 'KEPUB', 'CBZ', 'CBT', 'CBR', 'DJVU', 'DJV'}
|
||||
book_formats = list()
|
||||
if len(entry.data):
|
||||
for ele in iter(entry.data):
|
||||
@@ -237,7 +237,7 @@ def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id)
|
||||
return _("The requested file could not be read. Maybe wrong permissions?")
|
||||
|
||||
|
||||
def get_valid_filename(value, replace_whitespace=True, chars=128):
|
||||
def get_valid_filename(value, replace_whitespace=True, chars=128, force_unidecode=False):
|
||||
"""
|
||||
Returns the given string converted to a string that can be used for a clean
|
||||
filename. Limits num characters to 128 max.
|
||||
@@ -245,7 +245,7 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
|
||||
if value[-1:] == '.':
|
||||
value = value[:-1]+'_'
|
||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||
if config.config_unicode_filename:
|
||||
if config.config_unicode_filename or force_unidecode:
|
||||
value = (unidecode.unidecode(value))
|
||||
if replace_whitespace:
|
||||
# *+:\"/<>? are replaced by _
|
||||
@@ -513,7 +513,7 @@ def update_dir_structure_gdrive(book_id, first_author):
|
||||
book.path = new_authordir + '/' + book.path.split('/')[1]
|
||||
gd.updateDatabaseOnEdit(g_file['id'], book.path)
|
||||
else:
|
||||
return _('File %(file)s not found on Google Drive', file=authordir) # file not found'''
|
||||
return _('File %(file)s not found on Google Drive', file=authordir) # file not found
|
||||
if titledir != new_titledir or authordir != new_authordir :
|
||||
all_new_name = get_valid_filename(book.title, chars=42) + ' - ' \
|
||||
+ get_valid_filename(new_authordir, chars=42)
|
||||
@@ -905,7 +905,7 @@ def save_cover(img, book_path):
|
||||
else:
|
||||
imgc = Image(blob=io.BytesIO(img.content))
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace("rgb")
|
||||
imgc.transform_colorspace("srgb")
|
||||
img = imgc
|
||||
except (BlobError, MissingDelegateError):
|
||||
log.error("Invalid cover file content")
|
||||
@@ -974,7 +974,8 @@ def do_download_file(book, book_format, client, data, headers):
|
||||
# ToDo Check headers parameter
|
||||
for element in headers:
|
||||
response.headers[element[0]] = element[1]
|
||||
log.info('Downloading file: {}'.format(os.path.join(filename, book_name + "." + book_format)))
|
||||
log.info('Downloading file: \'%s\' by %s - %s', format(os.path.join(filename, book_name + "." + book_format)),
|
||||
current_user.name, request.headers.get('X-Forwarded-For', request.remote_addr))
|
||||
return response
|
||||
|
||||
|
||||
@@ -1104,11 +1105,14 @@ def get_download_link(book_id, book_format, client):
|
||||
file_name = book.title
|
||||
if len(book.authors) > 0:
|
||||
file_name = file_name + ' - ' + book.authors[0].name
|
||||
file_name = get_valid_filename(file_name, replace_whitespace=False)
|
||||
if client == "kindle":
|
||||
file_name = get_valid_filename(file_name, replace_whitespace=False, force_unidecode=True)
|
||||
else:
|
||||
file_name = quote(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" % (
|
||||
quote(file_name), book_format, quote(file_name), book_format)
|
||||
headers["Content-Disposition"] = ('attachment; filename="{}.{}"; filename*=UTF-8\'\'{}.{}').format(
|
||||
file_name, book_format, file_name, book_format)
|
||||
return do_download_file(book, book_format, client, data1, headers)
|
||||
else:
|
||||
log.error("Book id {} not found for downloading".format(book_id))
|
||||
|
||||
@@ -12,6 +12,384 @@
|
||||
# map iso639 language codes to language names, translated
|
||||
|
||||
LANGUAGE_NAMES = {
|
||||
"ar": {
|
||||
"abk": "Abkhazian",
|
||||
"ace": "Achinese",
|
||||
"ach": "Acoli",
|
||||
"ada": "Adangme",
|
||||
"ady": "Adyghe",
|
||||
"aar": "Afar",
|
||||
"afh": "Afrihili",
|
||||
"afr": "الأفريكانس",
|
||||
"ain": "Ainu (Japan)",
|
||||
"aka": "Akan",
|
||||
"akk": "Akkadian",
|
||||
"sqi": "Albanian",
|
||||
"ale": "Aleut",
|
||||
"amh": "Amharic",
|
||||
"anp": "Angika",
|
||||
"ara": "العربية",
|
||||
"arg": "Aragonese",
|
||||
"arp": "Arapaho",
|
||||
"arw": "Arawak",
|
||||
"hye": "الأرمينية",
|
||||
"asm": "Assamese",
|
||||
"ast": "Asturian",
|
||||
"ava": "Avaric",
|
||||
"ave": "Avestan",
|
||||
"awa": "Awadhi",
|
||||
"aym": "Aymara",
|
||||
"aze": "الأذربيجانية",
|
||||
"ban": "البالية",
|
||||
"bal": "Baluchi",
|
||||
"bam": "Bambara",
|
||||
"bas": "Basa (Cameroon)",
|
||||
"bak": "Bashkir",
|
||||
"eus": "الباسك",
|
||||
"bej": "Beja",
|
||||
"bel": "بلاروسي",
|
||||
"bem": "Bemba (Zambia)",
|
||||
"ben": "البنغالية",
|
||||
"bit": "Berinomo",
|
||||
"bho": "Bhojpuri",
|
||||
"bik": "Bikol",
|
||||
"byn": "Bilin",
|
||||
"bin": "Bini",
|
||||
"bis": "Bislama",
|
||||
"zbl": "Blissymbols",
|
||||
"bos": "Bosnian",
|
||||
"bra": "Braj",
|
||||
"bre": "البريتون",
|
||||
"bug": "البجينيز",
|
||||
"bul": "البلغارية",
|
||||
"bua": "Buriat",
|
||||
"mya": "البورمية",
|
||||
"cad": "Caddo",
|
||||
"cat": "الكاتالانية",
|
||||
"ceb": "Cebuano",
|
||||
"chg": "Chagatai",
|
||||
"cha": "Chamorro",
|
||||
"che": "Chechen",
|
||||
"chr": "الشيروكى",
|
||||
"chy": "Cheyenne",
|
||||
"chb": "Chibcha",
|
||||
"zho": "الصينية",
|
||||
"chn": "Chinook jargon",
|
||||
"chp": "Chipewyan",
|
||||
"cho": "Choctaw",
|
||||
"cht": "Cholón",
|
||||
"chk": "Chuukese",
|
||||
"chv": "Chuvash",
|
||||
"cop": "القبطية",
|
||||
"cor": "Cornish",
|
||||
"cos": "Corsican",
|
||||
"cre": "Cree",
|
||||
"mus": "Creek",
|
||||
"hrv": "الكرواتية",
|
||||
"ces": "التشيكية",
|
||||
"dak": "Dakota",
|
||||
"dan": "الدنماركية",
|
||||
"dar": "Dargwa",
|
||||
"del": "Delaware",
|
||||
"div": "Dhivehi",
|
||||
"din": "Dinka",
|
||||
"doi": "Dogri (macrolanguage)",
|
||||
"dgr": "Dogrib",
|
||||
"dua": "Duala",
|
||||
"nld": "الهولندية",
|
||||
"dse": "Dutch Sign Language",
|
||||
"dyu": "Dyula",
|
||||
"dzo": "الزونخاية",
|
||||
"efi": "Efik",
|
||||
"egy": "Egyptian (Ancient)",
|
||||
"eka": "Ekajuk",
|
||||
"elx": "Elamite",
|
||||
"eng": "الإنجليزية",
|
||||
"enu": "Enu",
|
||||
"myv": "Erzya",
|
||||
"epo": "الإسبرانتو",
|
||||
"est": "الإستونية",
|
||||
"ewe": "Ewe",
|
||||
"ewo": "Ewondo",
|
||||
"fan": "Fang (Equatorial Guinea)",
|
||||
"fat": "Fanti",
|
||||
"fao": "Faroese",
|
||||
"fij": "Fijian",
|
||||
"fil": "Filipino",
|
||||
"fin": "الفنلندية",
|
||||
"fon": "Fon",
|
||||
"fra": "الفرنسية",
|
||||
"fur": "Friulian",
|
||||
"ful": "Fulah",
|
||||
"gaa": "Ga",
|
||||
"glg": "Galician",
|
||||
"lug": "Ganda",
|
||||
"gay": "Gayo",
|
||||
"gba": "Gbaya (Central African Republic)",
|
||||
"hmj": "Ge",
|
||||
"gez": "Geez",
|
||||
"kat": "Georgian",
|
||||
"deu": "الألمانية",
|
||||
"gil": "Gilbertese",
|
||||
"gon": "Gondi",
|
||||
"gor": "Gorontalo",
|
||||
"got": "القوطية",
|
||||
"grb": "Grebo",
|
||||
"grn": "Guarani",
|
||||
"guj": "الغوجاراتية",
|
||||
"gwi": "Gwichʼin",
|
||||
"hai": "Haida",
|
||||
"hau": "Hausa",
|
||||
"haw": "Hawaiian",
|
||||
"heb": "العبرية",
|
||||
"her": "Herero",
|
||||
"hil": "Hiligaynon",
|
||||
"hin": "الهندية",
|
||||
"hmo": "Hiri Motu",
|
||||
"hit": "Hittite",
|
||||
"hmn": "Hmong",
|
||||
"hun": "الهنغارية (المجرية)",
|
||||
"hup": "Hupa",
|
||||
"iba": "Iban",
|
||||
"isl": "الآيسلندية",
|
||||
"ido": "Ido",
|
||||
"ibo": "Igbo",
|
||||
"ilo": "Iloko",
|
||||
"ind": "الإندونيسية",
|
||||
"inh": "Ingush",
|
||||
"ina": "Interlingua (International Auxiliary Language Association)",
|
||||
"ile": "Interlingue",
|
||||
"iku": "Inuktitut",
|
||||
"ipk": "Inupiaq",
|
||||
"gle": "الأيرلندية",
|
||||
"ita": "الإيطالية",
|
||||
"jpn": "اليابانية",
|
||||
"jav": "الجاوية",
|
||||
"jrb": "Judeo-Arabic",
|
||||
"jpr": "Judeo-Persian",
|
||||
"kbd": "Kabardian",
|
||||
"kab": "Kabyle",
|
||||
"kac": "Kachin",
|
||||
"kal": "Kalaallisut",
|
||||
"xal": "Kalmyk",
|
||||
"kam": "Kamba (Kenya)",
|
||||
"kan": "الكانادا",
|
||||
"kau": "Kanuri",
|
||||
"kaa": "Kara-Kalpak",
|
||||
"krc": "Karachay-Balkar",
|
||||
"krl": "Karelian",
|
||||
"kas": "Kashmiri",
|
||||
"csb": "Kashubian",
|
||||
"kaw": "Kawi",
|
||||
"kaz": "Kazakh",
|
||||
"kha": "Khasi",
|
||||
"kho": "Khotanese",
|
||||
"kik": "Kikuyu",
|
||||
"kmb": "Kimbundu",
|
||||
"kin": "Kinyarwanda",
|
||||
"kir": "Kirghiz",
|
||||
"tlh": "Klingon",
|
||||
"kom": "Komi",
|
||||
"kon": "Kongo",
|
||||
"kok": "Konkani (macrolanguage)",
|
||||
"kor": "الكورية",
|
||||
"kos": "Kosraean",
|
||||
"kpe": "Kpelle",
|
||||
"kua": "Kuanyama",
|
||||
"kum": "Kumyk",
|
||||
"kur": "الكردية",
|
||||
"kru": "Kurukh",
|
||||
"kut": "Kutenai",
|
||||
"lad": "Ladino",
|
||||
"lah": "Lahnda",
|
||||
"lam": "Lamba",
|
||||
"lao": "اللاو",
|
||||
"lat": "Latin",
|
||||
"lav": "اللاتفية",
|
||||
"lez": "Lezghian",
|
||||
"lim": "Limburgan",
|
||||
"lin": "Lingala",
|
||||
"lit": "الليتوانية",
|
||||
"jbo": "Lojban",
|
||||
"loz": "Lozi",
|
||||
"lub": "Luba-Katanga",
|
||||
"lua": "Luba-Lulua",
|
||||
"lui": "Luiseno",
|
||||
"smj": "Lule Sami",
|
||||
"lun": "Lunda",
|
||||
"luo": "Luo (Kenya and Tanzania)",
|
||||
"lus": "Lushai",
|
||||
"ltz": "Luxembourgish",
|
||||
"mkd": "المقدونية",
|
||||
"mad": "Madurese",
|
||||
"mag": "Magahi",
|
||||
"mai": "Maithili",
|
||||
"mak": "Makasar",
|
||||
"mlg": "Malagasy",
|
||||
"msa": "Malay (macrolanguage)",
|
||||
"mal": "الماليالام",
|
||||
"mlt": "المالطية",
|
||||
"mnc": "Manchu",
|
||||
"mdr": "Mandar",
|
||||
"man": "Mandingo",
|
||||
"mni": "Manipuri",
|
||||
"glv": "Manx",
|
||||
"mri": "الموري",
|
||||
"arn": "Mapudungun",
|
||||
"mar": "Marathi",
|
||||
"chm": "Mari (Russia)",
|
||||
"mah": "Marshallese",
|
||||
"mwr": "Marwari",
|
||||
"mas": "Masai",
|
||||
"men": "Mende (Sierra Leone)",
|
||||
"mic": "Mi'kmaq",
|
||||
"min": "Minangkabau",
|
||||
"mwl": "Mirandese",
|
||||
"moh": "Mohawk",
|
||||
"mdf": "Moksha",
|
||||
"lol": "Mongo",
|
||||
"mon": "المغولية",
|
||||
"mos": "Mossi",
|
||||
"mul": "Multiple languages",
|
||||
"nqo": "N'Ko",
|
||||
"nau": "Nauru",
|
||||
"nav": "Navajo",
|
||||
"ndo": "Ndonga",
|
||||
"nap": "Neapolitan",
|
||||
"nia": "Nias",
|
||||
"niu": "Niuean",
|
||||
"zxx": "No linguistic content",
|
||||
"nog": "Nogai",
|
||||
"nor": "النرويجية",
|
||||
"nob": "Norwegian Bokmål",
|
||||
"nno": "Norwegian Nynorsk",
|
||||
"nym": "Nyamwezi",
|
||||
"nya": "Nyanja",
|
||||
"nyn": "Nyankole",
|
||||
"nyo": "Nyoro",
|
||||
"nzi": "Nzima",
|
||||
"oci": "Occitan (post 1500)",
|
||||
"oji": "Ojibwa",
|
||||
"orm": "Oromo",
|
||||
"osa": "Osage",
|
||||
"oss": "Ossetian",
|
||||
"pal": "Pahlavi",
|
||||
"pau": "Palauan",
|
||||
"pli": "Pali",
|
||||
"pam": "Pampanga",
|
||||
"pag": "Pangasinan",
|
||||
"pan": "Panjabi",
|
||||
"pap": "Papiamento",
|
||||
"fas": "الفارسي",
|
||||
"phn": "الفينيقية",
|
||||
"pon": "Pohnpeian",
|
||||
"pol": "البولندية",
|
||||
"por": "البرتغالية",
|
||||
"pus": "Pashto",
|
||||
"que": "Quechua",
|
||||
"raj": "Rajasthani",
|
||||
"rap": "Rapanui",
|
||||
"ron": "الرومانية",
|
||||
"roh": "Romansh",
|
||||
"rom": "Romany",
|
||||
"run": "Rundi",
|
||||
"rus": "الروسية",
|
||||
"smo": "Samoan",
|
||||
"sad": "Sandawe",
|
||||
"sag": "Sango",
|
||||
"san": "Sanskrit",
|
||||
"sat": "Santali",
|
||||
"srd": "Sardinian",
|
||||
"sas": "Sasak",
|
||||
"sco": "Scots",
|
||||
"sel": "Selkup",
|
||||
"srp": "الصربية",
|
||||
"srr": "Serer",
|
||||
"shn": "Shan",
|
||||
"sna": "Shona",
|
||||
"scn": "Sicilian",
|
||||
"sid": "Sidamo",
|
||||
"bla": "Siksika",
|
||||
"snd": "Sindhi",
|
||||
"sin": "السينهالا",
|
||||
"den": "Slave (Athapascan)",
|
||||
"slk": "السلوفاكية",
|
||||
"slv": "السلوفينية",
|
||||
"sog": "Sogdian",
|
||||
"som": "Somali",
|
||||
"snk": "Soninke",
|
||||
"spa": "الأسبانية",
|
||||
"srn": "Sranan Tongo",
|
||||
"suk": "Sukuma",
|
||||
"sux": "Sumerian",
|
||||
"sun": "السوندية",
|
||||
"sus": "Susu",
|
||||
"swa": "Swahili (macrolanguage)",
|
||||
"ssw": "Swati",
|
||||
"swe": "السويدية",
|
||||
"syr": "السريانية",
|
||||
"tgl": "التاغالوغية",
|
||||
"tah": "Tahitian",
|
||||
"tgk": "Tajik",
|
||||
"tmh": "Tamashek",
|
||||
"tam": "التاميلية",
|
||||
"tat": "Tatar",
|
||||
"tel": "التيلوغو",
|
||||
"ter": "Tereno",
|
||||
"tet": "Tetum",
|
||||
"tha": "التايلاندية",
|
||||
"bod": "التبتية",
|
||||
"tig": "Tigre",
|
||||
"tir": "Tigrinya",
|
||||
"tem": "Timne",
|
||||
"tiv": "Tiv",
|
||||
"tli": "Tlingit",
|
||||
"tpi": "Tok Pisin",
|
||||
"tkl": "Tokelau",
|
||||
"tog": "Tonga (Nyasa)",
|
||||
"ton": "Tonga (Tonga Islands)",
|
||||
"tsi": "Tsimshian",
|
||||
"tso": "Tsonga",
|
||||
"tsn": "Tswana",
|
||||
"tum": "Tumbuka",
|
||||
"tur": "التركية",
|
||||
"tuk": "Turkmen",
|
||||
"tvl": "Tuvalu",
|
||||
"tyv": "Tuvinian",
|
||||
"twi": "Twi",
|
||||
"udm": "Udmurt",
|
||||
"uga": "الأجاريتيكية",
|
||||
"uig": "الاغورية",
|
||||
"ukr": "Ukrainian",
|
||||
"umb": "Umbundu",
|
||||
"mis": "Uncoded languages",
|
||||
"und": "Undetermined",
|
||||
"urd": "الاردية",
|
||||
"uzb": "Uzbek",
|
||||
"vai": "الفاي",
|
||||
"ven": "Venda",
|
||||
"vie": "الفييتنامية",
|
||||
"vol": "Volapük",
|
||||
"vot": "Votic",
|
||||
"wln": "الوالون",
|
||||
"war": "Waray (Philippines)",
|
||||
"was": "Washo",
|
||||
"cym": "الويلزية",
|
||||
"wal": "Wolaytta",
|
||||
"wol": "Wolof",
|
||||
"xho": "الهاوسا",
|
||||
"sah": "Yakut",
|
||||
"yao": "Yao",
|
||||
"yap": "Yapese",
|
||||
"yid": "Yiddish",
|
||||
"yor": "Yoruba",
|
||||
"zap": "Zapotec",
|
||||
"zza": "Zaza",
|
||||
"zen": "Zenaga",
|
||||
"zha": "Zhuang",
|
||||
"zul": "الزولو",
|
||||
"zun": "Zuni"
|
||||
},
|
||||
"cs": {
|
||||
"aar": "afarština",
|
||||
"abk": "abchazajština",
|
||||
|
||||
+3
-15
@@ -43,6 +43,8 @@ def url_for_other_page(page):
|
||||
args = request.view_args.copy()
|
||||
args['page'] = page
|
||||
for get, val in request.args.items():
|
||||
if get == "page":
|
||||
continue
|
||||
args[get] = val
|
||||
return url_for(request.endpoint, **args)
|
||||
|
||||
@@ -111,7 +113,7 @@ def yesno(value, yes, no):
|
||||
|
||||
@jinjia.app_template_filter('formatfloat')
|
||||
def formatfloat(value, decimals=1):
|
||||
if not value:
|
||||
if not value or (isinstance(value, str) and not value.is_numeric()):
|
||||
return value
|
||||
formated_value = ('{0:.' + str(decimals) + 'f}').format(value)
|
||||
if formated_value.endswith('.' + "0" * decimals):
|
||||
@@ -119,20 +121,6 @@ def formatfloat(value, decimals=1):
|
||||
return formated_value
|
||||
|
||||
|
||||
'''@jinjia.app_template_filter('formatseriesindex')
|
||||
def formatseriesindex_filter(series_index):
|
||||
if series_index:
|
||||
try:
|
||||
if int(series_index) - series_index == 0:
|
||||
return int(series_index)
|
||||
else:
|
||||
return series_index
|
||||
except (ValueError, TypeError):
|
||||
return series_index
|
||||
return 0
|
||||
'''
|
||||
|
||||
|
||||
@jinjia.app_template_filter('escapedlink')
|
||||
def escapedlink_filter(url, text):
|
||||
return "<a href='{}'>{}</a>".format(url, escape(text))
|
||||
|
||||
+96
-61
@@ -106,24 +106,29 @@ def make_request_to_kobo_store(sync_token=None):
|
||||
return store_response
|
||||
|
||||
|
||||
def redirect_or_proxy_request():
|
||||
def redirect_or_proxy_request(auth=False):
|
||||
if config.config_kobo_proxy:
|
||||
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.
|
||||
store_response = make_request_to_kobo_store()
|
||||
try:
|
||||
if request.method == "GET":
|
||||
alfa = redirect(get_store_url_for_current_request(), 307)
|
||||
return alfa
|
||||
else:
|
||||
# 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
|
||||
for header_key in CONNECTION_SPECIFIC_HEADERS:
|
||||
response_headers.pop(header_key, default=None)
|
||||
response_headers = store_response.headers
|
||||
for header_key in CONNECTION_SPECIFIC_HEADERS:
|
||||
response_headers.pop(header_key, default=None)
|
||||
|
||||
return make_response(
|
||||
store_response.content, store_response.status_code, response_headers.items()
|
||||
)
|
||||
else:
|
||||
return make_response(jsonify({}))
|
||||
return make_response(
|
||||
store_response.content, store_response.status_code, response_headers.items()
|
||||
)
|
||||
except Exception as e:
|
||||
log.error("Failed to receive or parse response from Kobo's endpoint: {}".format(e))
|
||||
if auth:
|
||||
return make_calibre_web_auth_response()
|
||||
return make_response(jsonify({}))
|
||||
|
||||
|
||||
def convert_to_kobo_timestamp_string(timestamp):
|
||||
@@ -323,7 +328,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||
sync_token.to_headers(extra_headers)
|
||||
|
||||
# log.debug("Kobo Sync Content: {}".format(sync_results))
|
||||
# jsonify decodes the unicode string different to what kobo expects
|
||||
# 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
|
||||
@@ -727,7 +732,7 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
||||
ub.session_commit()
|
||||
|
||||
|
||||
# Creates a Kobo "Tag" object from a ub.Shelf object
|
||||
# Creates a Kobo "Tag" object from an ub.Shelf object
|
||||
def create_kobo_tag(shelf):
|
||||
tag = {
|
||||
"Created": convert_to_kobo_timestamp_string(shelf.created),
|
||||
@@ -955,7 +960,8 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
|
||||
# TODO: Implement the following routes
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"])
|
||||
@kobo.route("/v1/library/<dummy>/preview", methods=["POST"])
|
||||
def HandleUnimplementedRequest(dummy=None):
|
||||
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)",
|
||||
request.base_url)
|
||||
@@ -1042,7 +1048,7 @@ def HandleAuthRequest():
|
||||
log.debug('Kobo Auth request')
|
||||
if config.config_kobo_proxy:
|
||||
try:
|
||||
return redirect_or_proxy_request()
|
||||
return redirect_or_proxy_request(auth=True)
|
||||
except Exception:
|
||||
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
|
||||
return make_calibre_web_auth_response()
|
||||
@@ -1126,25 +1132,37 @@ def download_book(book_id, book_format):
|
||||
|
||||
def NATIVE_KOBO_RESOURCES():
|
||||
return {
|
||||
"account_page": "https://secure.kobobooks.com/profile",
|
||||
"account_page": "https://www.kobo.com/account/settings",
|
||||
"account_page_rakuten": "https://my.rakuten.co.jp/",
|
||||
"add_device": "https://storeapi.kobo.com/v1/user/add-device",
|
||||
"add_entitlement": "https://storeapi.kobo.com/v1/library/{RevisionIds}",
|
||||
"affiliaterequest": "https://storeapi.kobo.com/v1/affiliate",
|
||||
"assets": "https://storeapi.kobo.com/v1/assets",
|
||||
"audiobook": "https://storeapi.kobo.com/v1/products/audiobooks/{ProductId}",
|
||||
"audiobook_detail_page": "https://www.kobo.com/{region}/{language}/audiobook/{slug}",
|
||||
"audiobook_landing_page": "https://www.kobo.com/{region}/{language}/audiobooks",
|
||||
"audiobook_preview": "https://storeapi.kobo.com/v1/products/audiobooks/{Id}/preview",
|
||||
"audiobook_purchase_withcredit": "https://storeapi.kobo.com/v1/store/audiobook/{Id}",
|
||||
"audiobook_subscription_orange_deal_inclusion_url": "https://authorize.kobo.com/inclusion",
|
||||
"authorproduct_recommendations": "https://storeapi.kobo.com/v1/products/books/authors/recommendations",
|
||||
"autocomplete": "https://storeapi.kobo.com/v1/products/autocomplete",
|
||||
"blackstone_header": {"key": "x-amz-request-payer", "value": "requester"},
|
||||
"blackstone_header": {
|
||||
"key": "x-amz-request-payer",
|
||||
"value": "requester"
|
||||
},
|
||||
"book": "https://storeapi.kobo.com/v1/products/books/{ProductId}",
|
||||
"book_detail_page": "https://store.kobobooks.com/{culture}/ebook/{slug}",
|
||||
"book_detail_page_rakuten": "https://books.rakuten.co.jp/rk/{crossrevisionid}",
|
||||
"book_landing_page": "https://store.kobobooks.com/ebooks",
|
||||
"book_detail_page": "https://www.kobo.com/{region}/{language}/ebook/{slug}",
|
||||
"book_detail_page_rakuten": "http://books.rakuten.co.jp/rk/{crossrevisionid}",
|
||||
"book_landing_page": "https://www.kobo.com/ebooks",
|
||||
"book_subscription": "https://storeapi.kobo.com/v1/products/books/subscriptions",
|
||||
"browse_history": "https://storeapi.kobo.com/v1/user/browsehistory",
|
||||
"categories": "https://storeapi.kobo.com/v1/categories",
|
||||
"categories_page": "https://store.kobobooks.com/ebooks/categories",
|
||||
"categories_page": "https://www.kobo.com/ebooks/categories",
|
||||
"category": "https://storeapi.kobo.com/v1/categories/{CategoryId}",
|
||||
"category_featured_lists": "https://storeapi.kobo.com/v1/categories/{CategoryId}/featured",
|
||||
"category_products": "https://storeapi.kobo.com/v1/categories/{CategoryId}/products",
|
||||
"checkout_borrowed_book": "https://storeapi.kobo.com/v1/library/borrow",
|
||||
"client_authd_referral": "https://authorize.kobo.com/api/AuthenticatedReferral/client/v1/getLink",
|
||||
"configuration_data": "https://storeapi.kobo.com/v1/configuration",
|
||||
"content_access_book": "https://storeapi.kobo.com/v1/products/books/{ProductId}/access",
|
||||
"customer_care_live_chat": "https://v2.zopim.com/widget/livechat.html?key=Y6gwUmnu4OATxN3Tli4Av9bYN319BTdO",
|
||||
@@ -1155,92 +1173,109 @@ def NATIVE_KOBO_RESOURCES():
|
||||
"delete_tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/items/delete",
|
||||
"device_auth": "https://storeapi.kobo.com/v1/auth/device",
|
||||
"device_refresh": "https://storeapi.kobo.com/v1/auth/refresh",
|
||||
"dictionary_host": "https://kbdownload1-a.akamaihd.net",
|
||||
"dictionary_host": "https://ereaderfiles.kobo.com",
|
||||
"discovery_host": "https://discovery.kobobooks.com",
|
||||
"ereaderdevices": "https://storeapi.kobo.com/v2/products/EReaderDeviceFeeds",
|
||||
"eula_page": "https://www.kobo.com/termsofuse?style=onestore",
|
||||
"exchange_auth": "https://storeapi.kobo.com/v1/auth/exchange",
|
||||
"external_book": "https://storeapi.kobo.com/v1/products/books/external/{Ids}",
|
||||
"facebook_sso_page":
|
||||
"https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://store.kobobooks.com/",
|
||||
"facebook_sso_page": "https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://kobo.com/",
|
||||
"featured_list": "https://storeapi.kobo.com/v1/products/featured/{FeaturedListId}",
|
||||
"featured_lists": "https://storeapi.kobo.com/v1/products/featured",
|
||||
"free_books_page": {
|
||||
"EN": "https://www.kobo.com/{region}/{language}/p/free-ebooks",
|
||||
"FR": "https://www.kobo.com/{region}/{language}/p/livres-gratuits",
|
||||
"IT": "https://www.kobo.com/{region}/{language}/p/libri-gratuiti",
|
||||
"NL": "https://www.kobo.com/{region}/{language}/"
|
||||
"List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg",
|
||||
"PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis",
|
||||
"NL": "https://www.kobo.com/{region}/{language}/List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg",
|
||||
"PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis"
|
||||
},
|
||||
"fte_feedback": "https://storeapi.kobo.com/v1/products/ftefeedback",
|
||||
"funnel_metrics": "https://storeapi.kobo.com/v1/funnelmetrics",
|
||||
"get_download_keys": "https://storeapi.kobo.com/v1/library/downloadkeys",
|
||||
"get_download_link": "https://storeapi.kobo.com/v1/library/downloadlink",
|
||||
"get_tests_request": "https://storeapi.kobo.com/v1/analytics/gettests",
|
||||
"giftcard_epd_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem-ereader",
|
||||
"giftcard_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem",
|
||||
"help_page": "https://www.kobo.com/help",
|
||||
"kobo_audiobooks_enabled": "False",
|
||||
"gpb_flow_enabled": "False",
|
||||
"help_page": "http://www.kobo.com/help",
|
||||
"image_host": "//cdn.kobo.com/book-images/",
|
||||
"image_url_quality_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg",
|
||||
"image_url_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/false/image.jpg",
|
||||
"kobo_audiobooks_credit_redemption": "False",
|
||||
"kobo_audiobooks_enabled": "True",
|
||||
"kobo_audiobooks_orange_deal_enabled": "False",
|
||||
"kobo_audiobooks_subscriptions_enabled": "False",
|
||||
"kobo_nativeborrow_enabled": "True",
|
||||
"kobo_display_price": "True",
|
||||
"kobo_dropbox_link_account_enabled": "False",
|
||||
"kobo_google_tax": "False",
|
||||
"kobo_googledrive_link_account_enabled": "False",
|
||||
"kobo_nativeborrow_enabled": "False",
|
||||
"kobo_onedrive_link_account_enabled": "False",
|
||||
"kobo_onestorelibrary_enabled": "False",
|
||||
"kobo_privacyCentre_url": "https://www.kobo.com/privacy",
|
||||
"kobo_redeem_enabled": "True",
|
||||
"kobo_shelfie_enabled": "False",
|
||||
"kobo_subscriptions_enabled": "False",
|
||||
"kobo_superpoints_enabled": "False",
|
||||
"kobo_subscriptions_enabled": "True",
|
||||
"kobo_superpoints_enabled": "True",
|
||||
"kobo_wishlist_enabled": "True",
|
||||
"library_book": "https://storeapi.kobo.com/v1/user/library/books/{LibraryItemId}",
|
||||
"library_items": "https://storeapi.kobo.com/v1/user/library",
|
||||
"library_metadata": "https://storeapi.kobo.com/v1/library/{Ids}/metadata",
|
||||
"library_prices": "https://storeapi.kobo.com/v1/user/library/previews/prices",
|
||||
"library_stack": "https://storeapi.kobo.com/v1/user/library/stacks/{LibraryItemId}",
|
||||
"library_search": "https://storeapi.kobo.com/v1/library/search",
|
||||
"library_sync": "https://storeapi.kobo.com/v1/library/sync",
|
||||
"love_dashboard_page": "https://store.kobobooks.com/{culture}/kobosuperpoints",
|
||||
"love_points_redemption_page":
|
||||
"https://store.kobobooks.com/{culture}/KoboSuperPointsRedemption?productId={ProductId}",
|
||||
"magazine_landing_page": "https://store.kobobooks.com/emagazines",
|
||||
"love_dashboard_page": "https://www.kobo.com/{region}/{language}/kobosuperpoints",
|
||||
"love_points_redemption_page": "https://www.kobo.com/{region}/{language}/KoboSuperPointsRedemption?productId={ProductId}",
|
||||
"magazine_landing_page": "https://www.kobo.com/emagazines",
|
||||
"more_sign_in_options": "https://authorize.kobo.com/signin?returnUrl=http://kobo.com/#allProviders",
|
||||
"notebooks": "https://storeapi.kobo.com/api/internal/notebooks",
|
||||
"notifications_registration_issue": "https://storeapi.kobo.com/v1/notifications/registration",
|
||||
"oauth_host": "https://oauth.kobo.com",
|
||||
"overdrive_account": "https://auth.overdrive.com/account",
|
||||
"overdrive_library": "https://{libraryKey}.auth.overdrive.com/library",
|
||||
"overdrive_library_finder_host": "https://libraryfinder.api.overdrive.com",
|
||||
"overdrive_thunder_host": "https://thunder.api.overdrive.com",
|
||||
"password_retrieval_page": "https://www.kobobooks.com/passwordretrieval.html",
|
||||
"password_retrieval_page": "https://www.kobo.com/passwordretrieval.html",
|
||||
"personalizedrecommendations": "https://storeapi.kobo.com/v2/users/personalizedrecommendations",
|
||||
"pocket_link_account_start": "https://authorize.kobo.com/{region}/{language}/linkpocket",
|
||||
"post_analytics_event": "https://storeapi.kobo.com/v1/analytics/event",
|
||||
"ppx_purchasing_url": "https://purchasing.kobo.com",
|
||||
"privacy_page": "https://www.kobo.com/privacypolicy?style=onestore",
|
||||
"product_nextread": "https://storeapi.kobo.com/v1/products/{ProductIds}/nextread",
|
||||
"product_prices": "https://storeapi.kobo.com/v1/products/{ProductIds}/prices",
|
||||
"product_recommendations": "https://storeapi.kobo.com/v1/products/{ProductId}/recommendations",
|
||||
"product_reviews": "https://storeapi.kobo.com/v1/products/{ProductIds}/reviews",
|
||||
"products": "https://storeapi.kobo.com/v1/products",
|
||||
"provider_external_sign_in_page":
|
||||
"https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://store.kobobooks.com/",
|
||||
"purchase_buy": "https://www.kobo.com/checkout/createpurchase/",
|
||||
"purchase_buy_templated": "https://www.kobo.com/{culture}/checkout/createpurchase/{ProductId}",
|
||||
"productsv2": "https://storeapi.kobo.com/v2/products",
|
||||
"provider_external_sign_in_page": "https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://kobo.com/",
|
||||
"quickbuy_checkout": "https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout",
|
||||
"quickbuy_create": "https://storeapi.kobo.com/v1/store/quickbuy/purchase",
|
||||
"rakuten_token_exchange": "https://storeapi.kobo.com/v1/auth/rakuten_token_exchange",
|
||||
"rating": "https://storeapi.kobo.com/v1/products/{ProductId}/rating/{Rating}",
|
||||
"reading_services_host": "https://readingservices.kobo.com",
|
||||
"reading_state": "https://storeapi.kobo.com/v1/library/{Ids}/state",
|
||||
"redeem_interstitial_page": "https://store.kobobooks.com",
|
||||
"registration_page": "https://authorize.kobo.com/signup?returnUrl=http://store.kobobooks.com/",
|
||||
"redeem_interstitial_page": "https://www.kobo.com",
|
||||
"registration_page": "https://authorize.kobo.com/signup?returnUrl=http://kobo.com/",
|
||||
"related_items": "https://storeapi.kobo.com/v1/products/{Id}/related",
|
||||
"remaining_book_series": "https://storeapi.kobo.com/v1/products/books/series/{SeriesId}",
|
||||
"rename_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}",
|
||||
"review": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}",
|
||||
"review_sentiment": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}/sentiment/{Sentiment}",
|
||||
"shelfie_recommendations": "https://storeapi.kobo.com/v1/user/recommendations/shelfie",
|
||||
"sign_in_page": "https://authorize.kobo.com/signin?returnUrl=http://store.kobobooks.com/",
|
||||
"sign_in_page": "https://authorize.kobo.com/signin?returnUrl=http://kobo.com/",
|
||||
"social_authorization_host": "https://social.kobobooks.com:8443",
|
||||
"social_host": "https://social.kobobooks.com",
|
||||
"stacks_host_productId": "https://store.kobobooks.com/collections/byproductid/",
|
||||
"store_home": "www.kobo.com/{region}/{language}",
|
||||
"store_host": "store.kobobooks.com",
|
||||
"store_newreleases": "https://store.kobobooks.com/{culture}/List/new-releases/961XUjtsU0qxkFItWOutGA",
|
||||
"store_search": "https://store.kobobooks.com/{culture}/Search?Query={query}",
|
||||
"store_top50": "https://store.kobobooks.com/{culture}/ebooks/Top",
|
||||
"store_host": "www.kobo.com",
|
||||
"store_newreleases": "https://www.kobo.com/{region}/{language}/List/new-releases/961XUjtsU0qxkFItWOutGA",
|
||||
"store_search": "https://www.kobo.com/{region}/{language}/Search?Query={query}",
|
||||
"store_top50": "https://www.kobo.com/{region}/{language}/ebooks/Top",
|
||||
"subs_landing_page": "https://www.kobo.com/{region}/{language}/plus",
|
||||
"subs_management_page": "https://www.kobo.com/{region}/{language}/account/subscriptions",
|
||||
"subs_plans_page": "https://www.kobo.com/{region}/{language}/plus/plans",
|
||||
"subs_purchase_buy_templated": "https://www.kobo.com/{region}/{language}/Checkoutoption/{ProductId}/{TierId}",
|
||||
"tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/Items",
|
||||
"tags": "https://storeapi.kobo.com/v1/library/tags",
|
||||
"taste_profile": "https://storeapi.kobo.com/v1/products/tasteprofile",
|
||||
"terms_of_sale_page": "https://authorize.kobo.com/{region}/{language}/terms/termsofsale",
|
||||
"update_accessibility_to_preview": "https://storeapi.kobo.com/v1/library/{EntitlementIds}/preview",
|
||||
"use_one_store": "False",
|
||||
"use_one_store": "True",
|
||||
"user_loyalty_benefits": "https://storeapi.kobo.com/v1/user/loyalty/benefits",
|
||||
"user_platform": "https://storeapi.kobo.com/v1/user/platform",
|
||||
"user_profile": "https://storeapi.kobo.com/v1/user/profile",
|
||||
@@ -1248,6 +1283,6 @@ def NATIVE_KOBO_RESOURCES():
|
||||
"user_recommendations": "https://storeapi.kobo.com/v1/user/recommendations",
|
||||
"user_reviews": "https://storeapi.kobo.com/v1/user/reviews",
|
||||
"user_wishlist": "https://storeapi.kobo.com/v1/user/wishlist",
|
||||
"userguide_host": "https://kbdownload1-a.akamaihd.net",
|
||||
"wishlist_page": "https://store.kobobooks.com/{region}/{language}/account/wishlist",
|
||||
"userguide_host": "https://ereaderfiles.kobo.com",
|
||||
"wishlist_page": "https://www.kobo.com/{region}/{language}/account/wishlist"
|
||||
}
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@
|
||||
This module also includes research notes into the auth protocol used by Kobo devices.
|
||||
|
||||
Log-in:
|
||||
When first booting a Kobo device the user must sign into a Kobo (or affiliate) account.
|
||||
When first booting a Kobo device the user must log in to a Kobo (or affiliate) account.
|
||||
Upon successful sign-in, the user is redirected to
|
||||
https://auth.kobobooks.com/CrossDomainSignIn?id=<some id>
|
||||
which serves the following response:
|
||||
@@ -41,7 +41,7 @@ issue for a few years now https://www.mobileread.com/forums/showpost.php?p=34768
|
||||
will still grant access given the userkey.)
|
||||
|
||||
Official Kobo Store Api authorization:
|
||||
* For most of the endpoints we care about (sync, metadata, tags, etc), the userKey is
|
||||
* For most of the endpoints we care about (sync, metadata, tags, etc.), the userKey is
|
||||
passed in the x-kobo-userkey header, and is sufficient to authorize the API call.
|
||||
* Some endpoints (e.g: AnnotationService) instead make use of Bearer tokens pass through
|
||||
an authorization header. To get a BearerToken, the device makes a POST request to the
|
||||
|
||||
+5
-11
@@ -29,7 +29,7 @@ from .constants import CONFIG_DIR as _CONFIG_DIR
|
||||
ACCESS_FORMATTER_GEVENT = Formatter("%(message)s")
|
||||
ACCESS_FORMATTER_TORNADO = Formatter("[%(asctime)s] %(message)s")
|
||||
|
||||
FORMATTER = Formatter("[%(asctime)s] %(levelname)5s {%(name)s:%(lineno)d} %(message)s")
|
||||
FORMATTER = Formatter("[%(asctime)s] %(levelname)5s {%(filename)s:%(lineno)d} %(message)s")
|
||||
DEFAULT_LOG_LEVEL = logging.INFO
|
||||
DEFAULT_LOG_FILE = os.path.join(_CONFIG_DIR, "calibre-web.log")
|
||||
DEFAULT_ACCESS_LOG = os.path.join(_CONFIG_DIR, "access.log")
|
||||
@@ -42,18 +42,12 @@ logging.addLevelName(logging.CRITICAL, "CRIT")
|
||||
|
||||
class _Logger(logging.Logger):
|
||||
|
||||
def error_or_exception(self, message, stacklevel=2, *args, **kwargs):
|
||||
def error_or_exception(self, message, stacklevel=1, *args, **kwargs):
|
||||
is_debug = self.getEffectiveLevel() <= logging.DEBUG
|
||||
if sys.version_info > (3, 7):
|
||||
if is_debug:
|
||||
self.exception(message, stacklevel=stacklevel, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, stacklevel=stacklevel, *args, **kwargs)
|
||||
if not is_debug:
|
||||
self.exception(message, stacklevel=stacklevel, *args, **kwargs)
|
||||
else:
|
||||
if is_debug:
|
||||
self.exception(message, stack_info=True, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, *args, **kwargs)
|
||||
self.error(message, stacklevel=stacklevel, *args, **kwargs)
|
||||
|
||||
def debug_no_auth(self, message, *args, **kwargs):
|
||||
message = message.strip("\r\n")
|
||||
|
||||
@@ -31,6 +31,7 @@ def main():
|
||||
app = create_app()
|
||||
|
||||
from .web import web
|
||||
from .basic import basic
|
||||
from .opds import opds
|
||||
from .admin import admi
|
||||
from .gdrive import gdrive
|
||||
@@ -64,6 +65,7 @@ def main():
|
||||
app.register_blueprint(search)
|
||||
app.register_blueprint(tasks)
|
||||
app.register_blueprint(web)
|
||||
app.register_blueprint(basic)
|
||||
app.register_blueprint(opds)
|
||||
limiter.limit("3/minute", key_func=request_username)(opds)
|
||||
app.register_blueprint(jinjia)
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ class OAuthBackend(SQLAlchemyBackend):
|
||||
Stores and retrieves OAuth tokens using a relational database through
|
||||
the `SQLAlchemy`_ ORM.
|
||||
|
||||
.. _SQLAlchemy: https://www.sqlalchemy.org/
|
||||
_SQLAlchemy: https://www.sqlalchemy.org/
|
||||
"""
|
||||
def __init__(self, model, session, provider_id,
|
||||
user=None, user_id=None, user_required=None, anon_user=None,
|
||||
|
||||
+3
-3
@@ -21,10 +21,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import json
|
||||
# import json
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
from flask import Blueprint, request, render_template, make_response, abort, Response, g
|
||||
from flask import Blueprint, request, render_template, make_response, abort, g, jsonify
|
||||
from flask_babel import get_locale
|
||||
from flask_babel import gettext as _
|
||||
|
||||
@@ -451,7 +451,7 @@ def get_database_stats():
|
||||
stat['authors'] = calibre_db.session.query(db.Authors).count()
|
||||
stat['categories'] = calibre_db.session.query(db.Tags).count()
|
||||
stat['series'] = calibre_db.session.query(db.Series).count()
|
||||
return Response(json.dumps(stat), mimetype="application/json")
|
||||
return make_response(jsonify(stat))
|
||||
|
||||
|
||||
@opds.route("/opds/thumb_240_240/<book_id>")
|
||||
|
||||
+2
-2
@@ -41,9 +41,9 @@ class ReverseProxied(object):
|
||||
"""Wrap the application in this middleware and configure the
|
||||
front-end server to add these headers, to let you quietly bind
|
||||
this to a URL other than / and to an HTTP scheme that is
|
||||
different than what is used locally.
|
||||
different from what is used locally.
|
||||
|
||||
Code courtesy of: http://flask.pocoo.org/snippets/35/
|
||||
Code courtesy of: https://flask.pocoo.org/snippets/35/
|
||||
|
||||
In nginx:
|
||||
location /myprefix {
|
||||
|
||||
@@ -23,7 +23,7 @@ import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from flask import Blueprint, Response, request, url_for
|
||||
from flask import Blueprint, request, url_for, make_response, jsonify
|
||||
from .cw_login import current_user
|
||||
from flask_babel import get_locale
|
||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||
@@ -89,7 +89,7 @@ def metadata_provider():
|
||||
provider.append(
|
||||
{"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__}
|
||||
)
|
||||
return Response(json.dumps(provider), mimetype="application/json")
|
||||
return make_response(jsonify(provider))
|
||||
|
||||
|
||||
@meta.route("/metadata/provider", methods=["POST"])
|
||||
@@ -114,9 +114,7 @@ def metadata_change_active_provider(prov_name):
|
||||
provider = next((c for c in cl if c.__id__ == prov_name), None)
|
||||
if provider is not None:
|
||||
data = provider.search(new_state.get("query", ""))
|
||||
return Response(
|
||||
json.dumps([asdict(x) for x in data]), mimetype="application/json"
|
||||
)
|
||||
return make_response(jsonify([asdict(x) for x in data]))
|
||||
return ""
|
||||
|
||||
|
||||
@@ -138,4 +136,4 @@ def metadata_search():
|
||||
}
|
||||
for future in concurrent.futures.as_completed(meta):
|
||||
data.extend([asdict(x) for x in future.result() if x])
|
||||
return Response(json.dumps(data), mimetype="application/json")
|
||||
return make_response(jsonify(data))
|
||||
|
||||
@@ -92,7 +92,7 @@ def send_messsage(token, msg):
|
||||
if creds and creds.expired and creds.refresh_token:
|
||||
creds.refresh(Request())
|
||||
service = build('gmail', 'v1', credentials=creds)
|
||||
message_as_bytes = msg.as_bytes() # the message should converted from string to bytes.
|
||||
message_as_bytes = msg.as_bytes() # the message should be converted from string to bytes.
|
||||
message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) # encode in base64 (printable letters coding)
|
||||
raw = message_as_base64.decode() # convert to something JSON serializable
|
||||
body = {'raw': raw}
|
||||
|
||||
@@ -117,7 +117,7 @@ def get_author_info(author_name):
|
||||
|
||||
|
||||
def get_other_books(author_info, library_books=None):
|
||||
# Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates
|
||||
# Get all identifiers (ISBN, Goodreads, etc.) and filter author's books by that list so we show fewer duplicates
|
||||
# Note: Not all images will be shown, even though they're available on Goodreads.com.
|
||||
# See https://www.goodreads.com/topic/show/18213769-goodreads-book-images
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ class CalibreTask:
|
||||
self.run(*args)
|
||||
except Exception as ex:
|
||||
self._handleError(str(ex))
|
||||
log.error_or_exception(ex)
|
||||
log.exception(ex)
|
||||
|
||||
self.end_time = datetime.now()
|
||||
|
||||
@@ -235,7 +235,7 @@ class CalibreTask:
|
||||
|
||||
@property
|
||||
def dead(self):
|
||||
"""Determines whether or not this task can be garbage collected
|
||||
"""Determines whether this task can be garbage collected
|
||||
|
||||
We have a separate dictating this because there may be certain tasks that want to override this
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
height: 75px;
|
||||
padding: 5px 20px;
|
||||
width: 100%;
|
||||
display: table;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
nav > * {
|
||||
display: inline-block;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
float: none;
|
||||
text-align: center;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
nav > *:first-child {
|
||||
text-align: left;
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
.theme{
|
||||
text-align: center;
|
||||
margin: 10px;
|
||||
}
|
||||
nav > *:last-child {
|
||||
text-align: right;
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
nav > a {
|
||||
color: black;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: auto auto;
|
||||
}
|
||||
|
||||
form > input {
|
||||
width: 18ch;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
form > * {
|
||||
height: 50px;
|
||||
background-color: white;
|
||||
border-radius: 0;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
form > span {
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
padding: 0 10px;
|
||||
margin: 0;
|
||||
width: 160px;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 150px;
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.listing {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
padding: 10px 0;
|
||||
height: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pagination > div {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.pagination > div:last-child {
|
||||
float: right;
|
||||
}
|
||||
@@ -7153,12 +7153,11 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
}
|
||||
|
||||
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 {
|
||||
max-width: 130px;
|
||||
width: 130px;
|
||||
height: 180px;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
max-width: unset;
|
||||
width: 100%;
|
||||
height: unset;
|
||||
padding: 15px;
|
||||
position: absolute
|
||||
}
|
||||
|
||||
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9 {
|
||||
@@ -7167,10 +7166,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
width: 100%
|
||||
}
|
||||
|
||||
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9 > .form-group:nth-child(1), body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9 > .form-group:nth-child(2), body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9 > .form-group:nth-child(1), body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9 > .form-group:nth-child(2) {
|
||||
padding-left: 120px
|
||||
}
|
||||
|
||||
#deleteButton, body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete {
|
||||
top: 48px;
|
||||
height: 42px
|
||||
|
||||
Vendored
-1
@@ -157,7 +157,6 @@ fieldset[disabled] .twitter-typeahead .tt-input {
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
|
||||
@@ -511,12 +511,12 @@ input:-moz-placeholder { color: #454545; }
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 630px;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100vw;
|
||||
height: auto;
|
||||
max-width: 630px;
|
||||
z-index: 2000;
|
||||
visibility: hidden;
|
||||
margin-left: -320px;
|
||||
margin-top: -160px;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.ar={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0}}(jQuery);
|
||||
+2
-2
@@ -16667,7 +16667,7 @@ const PDFWorkerUtil = {
|
||||
{
|
||||
if (isNodeJS) {
|
||||
PDFWorkerUtil.isWorkerDisabled = true;
|
||||
GlobalWorkerOptions.workerSrc ||= "./pdf.worker.mjs";
|
||||
GlobalWorkerOptions.workerSrc ||= "./pdf.worker.js";
|
||||
}
|
||||
PDFWorkerUtil.isSameOrigin = function (baseUrl, otherUrl) {
|
||||
let base;
|
||||
@@ -24745,4 +24745,4 @@ var __webpack_exports__shadow = __webpack_exports__.shadow;
|
||||
var __webpack_exports__version = __webpack_exports__.version;
|
||||
export { __webpack_exports__AbortException as AbortException, __webpack_exports__AnnotationEditorLayer as AnnotationEditorLayer, __webpack_exports__AnnotationEditorParamsType as AnnotationEditorParamsType, __webpack_exports__AnnotationEditorType as AnnotationEditorType, __webpack_exports__AnnotationEditorUIManager as AnnotationEditorUIManager, __webpack_exports__AnnotationLayer as AnnotationLayer, __webpack_exports__AnnotationMode as AnnotationMode, __webpack_exports__CMapCompressionType as CMapCompressionType, __webpack_exports__ColorPicker as ColorPicker, __webpack_exports__DOMSVGFactory as DOMSVGFactory, __webpack_exports__DrawLayer as DrawLayer, __webpack_exports__FeatureTest as FeatureTest, __webpack_exports__GlobalWorkerOptions as GlobalWorkerOptions, __webpack_exports__ImageKind as ImageKind, __webpack_exports__InvalidPDFException as InvalidPDFException, __webpack_exports__MissingPDFException as MissingPDFException, __webpack_exports__OPS as OPS, __webpack_exports__PDFDataRangeTransport as PDFDataRangeTransport, __webpack_exports__PDFDateString as PDFDateString, __webpack_exports__PDFWorker as PDFWorker, __webpack_exports__PasswordResponses as PasswordResponses, __webpack_exports__PermissionFlag as PermissionFlag, __webpack_exports__PixelsPerInch as PixelsPerInch, __webpack_exports__RenderingCancelledException as RenderingCancelledException, __webpack_exports__TextLayer as TextLayer, __webpack_exports__UnexpectedResponseException as UnexpectedResponseException, __webpack_exports__Util as Util, __webpack_exports__VerbosityLevel as VerbosityLevel, __webpack_exports__XfaLayer as XfaLayer, __webpack_exports__build as build, __webpack_exports__createValidAbsoluteUrl as createValidAbsoluteUrl, __webpack_exports__fetchData as fetchData, __webpack_exports__getDocument as getDocument, __webpack_exports__getFilenameFromUrl as getFilenameFromUrl, __webpack_exports__getPdfFilenameFromUrl as getPdfFilenameFromUrl, __webpack_exports__getXfaPageViewport as getXfaPageViewport, __webpack_exports__isDataScheme as isDataScheme, __webpack_exports__isPdfFile as isPdfFile, __webpack_exports__noContextMenu as noContextMenu, __webpack_exports__normalizeUnicode as normalizeUnicode, __webpack_exports__setLayerDimensions as setLayerDimensions, __webpack_exports__shadow as shadow, __webpack_exports__version as version };
|
||||
|
||||
//# sourceMappingURL=pdf.mjs.map
|
||||
//# sourceMappingURL=pdf.mjs.map
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -183,7 +183,7 @@ try {
|
||||
];
|
||||
$.each(sequences, function(ignore, sequence) {
|
||||
for (j = 0; j < word.length - 2; j += 1) {
|
||||
// iterate the word trough a sliding window of size 3:
|
||||
// iterate the word through a sliding window of size 3:
|
||||
if (
|
||||
sequence.indexOf(
|
||||
word.toLowerCase().substring(j, j + 3)
|
||||
|
||||
@@ -828,7 +828,7 @@ const defaultOptions = {
|
||||
kind: OptionKind.WORKER
|
||||
},
|
||||
workerSrc: {
|
||||
value: "../build/pdf.worker.mjs",
|
||||
value: "../build/pdf.worker.js",
|
||||
kind: OptionKind.WORKER
|
||||
}
|
||||
};
|
||||
+10
-10
@@ -49,7 +49,7 @@ function elementSorter(a, b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Generic control/related handler to show/hide fields based on a checkbox' value
|
||||
// Generic control/related handler to show/hide fields based on a 'checkbox' value
|
||||
// e.g.
|
||||
// <input type="checkbox" data-control="stuff-to-show">
|
||||
// <div data-related="stuff-to-show">...</div>
|
||||
@@ -63,7 +63,7 @@ $(document).on("change", "input[type=\"checkbox\"][data-control]", function () {
|
||||
});
|
||||
});
|
||||
|
||||
// Generic control/related handler to show/hide fields based on a select' value
|
||||
// Generic control/related handler to show/hide fields based on a 'select' value
|
||||
$(document).on("change", "select[data-control]", function() {
|
||||
var $this = $(this);
|
||||
var name = $this.data("control");
|
||||
@@ -79,7 +79,7 @@ $(document).on("change", "select[data-control]", function() {
|
||||
}
|
||||
});
|
||||
|
||||
// Generic control/related handler to show/hide fields based on a select' value
|
||||
// Generic control/related handler to show/hide fields based on a 'select' value
|
||||
// this one is made to show all values if select value is not 0
|
||||
$(document).on("change", "select[data-controlall]", function() {
|
||||
var $this = $(this);
|
||||
@@ -726,20 +726,20 @@ $(function() {
|
||||
url: getPath() + "/ajax/simulatedbchange",
|
||||
data: {config_calibre_dir: $("#config_calibre_dir").val(), csrf_token: $("input[name='csrf_token']").val()},
|
||||
success: function success(data) {
|
||||
if ( data.change ) {
|
||||
if ( data.valid ) {
|
||||
if ( !data.valid ) {
|
||||
$("#InvalidDialog").modal('show');
|
||||
}
|
||||
else{
|
||||
if ( data.change ) {
|
||||
confirmDialog(
|
||||
"db_submit",
|
||||
"GeneralChangeModal",
|
||||
0,
|
||||
changeDbSettings
|
||||
);
|
||||
}
|
||||
else {
|
||||
$("#InvalidDialog").modal('show');
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
changeDbSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* waits until queue is finished, meaning the book is done loading
|
||||
* @param callback
|
||||
*/
|
||||
function qFinished(callback){
|
||||
let timeout=setInterval(()=>{
|
||||
if(reader.rendition.q.running===undefined)
|
||||
clearInterval(timeout);
|
||||
callback();
|
||||
},300
|
||||
)
|
||||
}
|
||||
|
||||
function calculateProgress(){
|
||||
let data=reader.rendition.location.end;
|
||||
return Math.round(epub.locations.percentageFromCfi(data.cfi)*100);
|
||||
}
|
||||
|
||||
// register new event emitter locationchange that fires on urlchange
|
||||
// source: https://stackoverflow.com/a/52809105/21941129
|
||||
(() => {
|
||||
let oldPushState = history.pushState;
|
||||
history.pushState = function pushState() {
|
||||
let ret = oldPushState.apply(this, arguments);
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
return ret;
|
||||
};
|
||||
|
||||
let oldReplaceState = history.replaceState;
|
||||
history.replaceState = function replaceState() {
|
||||
let ret = oldReplaceState.apply(this, arguments);
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
return ret;
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
});
|
||||
})();
|
||||
|
||||
window.addEventListener('locationchange',()=>{
|
||||
let newPos=calculateProgress();
|
||||
progressDiv.textContent=newPos+"%";
|
||||
});
|
||||
|
||||
var epub=ePub(calibre.bookUrl)
|
||||
|
||||
let progressDiv=document.getElementById("progress");
|
||||
|
||||
qFinished(()=>{
|
||||
epub.locations.generate().then(()=> {
|
||||
window.dispatchEvent(new Event('locationchange'))
|
||||
});
|
||||
})
|
||||
@@ -52,6 +52,32 @@ var reader;
|
||||
}
|
||||
});
|
||||
|
||||
// Update progress percentage
|
||||
let progressDiv = document.getElementById("progress");
|
||||
reader.book.ready.then((()=>{
|
||||
let locations_key = reader.book.key()+'-locations';
|
||||
let stored_locations = localStorage.getItem(locations_key);
|
||||
let make_locations, save_locations;
|
||||
if (stored_locations) {
|
||||
make_locations = Promise.resolve(reader.book.locations.load(stored_locations));
|
||||
// No-op because locations are already saved
|
||||
save_locations = ()=>{};
|
||||
} else {
|
||||
make_locations = reader.book.locations.generate();
|
||||
save_locations = ()=>{
|
||||
localStorage.setItem(locations_key, reader.book.locations.save());
|
||||
};
|
||||
}
|
||||
make_locations.then(()=>{
|
||||
reader.rendition.on('relocated', (location)=>{
|
||||
let percentage = Math.round(location.end.percentage*100);
|
||||
progressDiv.textContent=percentage+"%";
|
||||
});
|
||||
reader.rendition.reportLocation();
|
||||
progressDiv.style.visibility = "visible";
|
||||
}).then(save_locations);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @param {string} action - Add or remove bookmark
|
||||
* @param {string|int} location - Location or zero
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// register new event emitter locationchange that fires on urlchange
|
||||
// source: https://stackoverflow.com/a/52809105/21941129
|
||||
(() => {
|
||||
let oldPushState = history.pushState;
|
||||
history.pushState = function pushState() {
|
||||
let ret = oldPushState.apply(this, arguments);
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
return ret;
|
||||
};
|
||||
|
||||
let oldReplaceState = history.replaceState;
|
||||
history.replaceState = function replaceState() {
|
||||
let ret = oldReplaceState.apply(this, arguments);
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
return ret;
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
});
|
||||
})();
|
||||
@@ -103,7 +103,7 @@ $(function() {
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../ajax/mergebooks",
|
||||
url: getPath() + "/ajax/mergebooks",
|
||||
data: JSON.stringify({"Merge_books":selections}),
|
||||
success: function success() {
|
||||
$("#books-table").bootstrapTable("refresh");
|
||||
@@ -122,7 +122,7 @@ $(function() {
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../ajax/simulatemerge",
|
||||
url: getPath() + "/ajax/simulatemerge",
|
||||
data: JSON.stringify({"Merge_books":selections}),
|
||||
success: function success(booTitles) {
|
||||
$('#merge_from').empty();
|
||||
@@ -140,7 +140,7 @@ $(function() {
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../ajax/xchange",
|
||||
url: getPath() + "/ajax/xchange",
|
||||
data: JSON.stringify({"xchange":selections}),
|
||||
success: function success() {
|
||||
$("#books-table").bootstrapTable("refresh");
|
||||
|
||||
+103
-97
@@ -28,7 +28,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from flask_babel import lazy_gettext as N_
|
||||
|
||||
from cps.services.worker import CalibreTask
|
||||
from cps import db
|
||||
from cps import db, app
|
||||
from cps import logger, config
|
||||
from cps.subproc_wrapper import process_open
|
||||
from flask_babel import gettext as _
|
||||
@@ -60,36 +60,39 @@ class TaskConvert(CalibreTask):
|
||||
self.results = dict()
|
||||
|
||||
def run(self, worker_thread):
|
||||
df_cover = None
|
||||
cur_book = None
|
||||
self.worker_thread = worker_thread
|
||||
if config.config_use_google_drive:
|
||||
worker_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
cur_book = worker_db.get_book(self.book_id)
|
||||
self.title = cur_book.title
|
||||
data = worker_db.get_book_format(self.book_id, self.settings['old_book_format'])
|
||||
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
|
||||
data.name + "." + self.settings['old_book_format'].lower())
|
||||
df_cover = gdriveutils.getFileFromEbooksFolder(cur_book.path, "cover.jpg")
|
||||
if df:
|
||||
datafile_cover = None
|
||||
datafile = os.path.join(config.get_book_path(),
|
||||
cur_book.path,
|
||||
data.name + "." + self.settings['old_book_format'].lower())
|
||||
if df_cover:
|
||||
datafile_cover = os.path.join(config.get_book_path(),
|
||||
cur_book.path, "cover.jpg")
|
||||
if not os.path.exists(os.path.join(config.get_book_path(), cur_book.path)):
|
||||
os.makedirs(os.path.join(config.get_book_path(), cur_book.path))
|
||||
df.GetContentFile(datafile)
|
||||
if df_cover:
|
||||
df_cover.GetContentFile(datafile_cover)
|
||||
worker_db.session.close()
|
||||
else:
|
||||
# ToDo Include cover in error handling
|
||||
error_message = _("%(format)s not found on Google Drive: %(fn)s",
|
||||
format=self.settings['old_book_format'],
|
||||
fn=data.name + "." + self.settings['old_book_format'].lower())
|
||||
worker_db.session.close()
|
||||
return self._handleError(error_message)
|
||||
with app.app_context():
|
||||
worker_db = db.CalibreDB(app)
|
||||
cur_book = worker_db.get_book(self.book_id)
|
||||
self.title = cur_book.title
|
||||
data = worker_db.get_book_format(self.book_id, self.settings['old_book_format'])
|
||||
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
|
||||
data.name + "." + self.settings['old_book_format'].lower())
|
||||
df_cover = gdriveutils.getFileFromEbooksFolder(cur_book.path, "cover.jpg")
|
||||
if df:
|
||||
datafile_cover = None
|
||||
datafile = os.path.join(config.get_book_path(),
|
||||
cur_book.path,
|
||||
data.name + "." + self.settings['old_book_format'].lower())
|
||||
if df_cover:
|
||||
datafile_cover = os.path.join(config.get_book_path(),
|
||||
cur_book.path, "cover.jpg")
|
||||
if not os.path.exists(os.path.join(config.get_book_path(), cur_book.path)):
|
||||
os.makedirs(os.path.join(config.get_book_path(), cur_book.path))
|
||||
df.GetContentFile(datafile)
|
||||
if df_cover:
|
||||
df_cover.GetContentFile(datafile_cover)
|
||||
# worker_db.session.close()
|
||||
else:
|
||||
# ToDo Include cover in error handling
|
||||
error_message = _("%(format)s not found on Google Drive: %(fn)s",
|
||||
format=self.settings['old_book_format'],
|
||||
fn=data.name + "." + self.settings['old_book_format'].lower())
|
||||
# worker_db.session.close()
|
||||
return self._handleError(error_message)
|
||||
|
||||
filename = self._convert_ebook_format()
|
||||
if config.config_use_google_drive:
|
||||
@@ -106,7 +109,7 @@ class TaskConvert(CalibreTask):
|
||||
# if we're sending to E-Reader after converting, create a one-off task and run it immediately
|
||||
# todo: figure out how to incorporate this into the progress
|
||||
try:
|
||||
EmailText = N_(u"%(book)s send to E-Reader", book=escape(self.title))
|
||||
EmailText = N_(u"%(book)s send to E-Reader", book=escape(self.title))
|
||||
for email in self.ereader_mail.split(','):
|
||||
email = strip_whitespaces(email)
|
||||
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
||||
@@ -124,88 +127,89 @@ class TaskConvert(CalibreTask):
|
||||
|
||||
def _convert_ebook_format(self):
|
||||
error_message = None
|
||||
local_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
file_path = self.file_path
|
||||
book_id = self.book_id
|
||||
format_old_ext = '.' + self.settings['old_book_format'].lower()
|
||||
format_new_ext = '.' + self.settings['new_book_format'].lower()
|
||||
with app.app_context():
|
||||
local_db = db.CalibreDB(app)
|
||||
file_path = self.file_path
|
||||
book_id = self.book_id
|
||||
format_old_ext = '.' + self.settings['old_book_format'].lower()
|
||||
format_new_ext = '.' + self.settings['new_book_format'].lower()
|
||||
|
||||
# check to see if destination format already exists - or if book is in database
|
||||
# if it does - mark the conversion task as complete and return a success
|
||||
# this will allow to send to E-Reader workflow to continue to work
|
||||
if os.path.isfile(file_path + format_new_ext) or\
|
||||
local_db.get_book_format(self.book_id, self.settings['new_book_format']):
|
||||
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
||||
cur_book = local_db.get_book(book_id)
|
||||
self.title = cur_book.title
|
||||
self.results['path'] = cur_book.path
|
||||
self.results['title'] = self.title
|
||||
new_format = local_db.session.query(db.Data).filter(db.Data.book == book_id)\
|
||||
.filter(db.Data.format == self.settings['new_book_format'].upper()).one_or_none()
|
||||
if not new_format:
|
||||
new_format = db.Data(name=os.path.basename(file_path),
|
||||
book_format=self.settings['new_book_format'].upper(),
|
||||
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
|
||||
try:
|
||||
local_db.session.merge(new_format)
|
||||
local_db.session.commit()
|
||||
except SQLAlchemyError as e:
|
||||
local_db.session.rollback()
|
||||
log.error("Database error: %s", e)
|
||||
local_db.session.close()
|
||||
self._handleError(N_("Oops! Database Error: %(error)s.", error=e))
|
||||
return
|
||||
self._handleSuccess()
|
||||
local_db.session.close()
|
||||
return os.path.basename(file_path + format_new_ext)
|
||||
else:
|
||||
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
|
||||
book_id,
|
||||
format_new_ext)
|
||||
|
||||
if config.config_kepubifypath and format_old_ext == '.epub' and format_new_ext == '.kepub':
|
||||
check, error_message = self._convert_kepubify(file_path,
|
||||
format_old_ext,
|
||||
format_new_ext)
|
||||
else:
|
||||
# check if calibre converter-executable is existing
|
||||
if not os.path.exists(config.config_converterpath):
|
||||
self._handleError(N_("Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
|
||||
return
|
||||
has_cover = local_db.get_book(book_id).has_cover
|
||||
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext, has_cover)
|
||||
|
||||
if check == 0:
|
||||
cur_book = local_db.get_book(book_id)
|
||||
if os.path.isfile(file_path + format_new_ext):
|
||||
new_format = local_db.session.query(db.Data).filter(db.Data.book == book_id) \
|
||||
# check to see if destination format already exists - or if book is in database
|
||||
# if it does - mark the conversion task as complete and return a success
|
||||
# this will allow to send to E-Reader workflow to continue to work
|
||||
if os.path.isfile(file_path + format_new_ext) or\
|
||||
local_db.get_book_format(self.book_id, self.settings['new_book_format']):
|
||||
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
||||
cur_book = local_db.get_book(book_id)
|
||||
self.title = cur_book.title
|
||||
self.results['path'] = cur_book.path
|
||||
self.results['title'] = self.title
|
||||
new_format = local_db.session.query(db.Data).filter(db.Data.book == book_id)\
|
||||
.filter(db.Data.format == self.settings['new_book_format'].upper()).one_or_none()
|
||||
if not new_format:
|
||||
new_format = db.Data(name=cur_book.data[0].name,
|
||||
new_format = db.Data(name=os.path.basename(file_path),
|
||||
book_format=self.settings['new_book_format'].upper(),
|
||||
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
|
||||
try:
|
||||
local_db.session.merge(new_format)
|
||||
local_db.session.commit()
|
||||
if self.settings['new_book_format'].upper() in ['KEPUB', 'EPUB', 'EPUB3']:
|
||||
ub_session = init_db_thread()
|
||||
remove_synced_book(book_id, True, ub_session)
|
||||
ub_session.close()
|
||||
except SQLAlchemyError as e:
|
||||
local_db.session.rollback()
|
||||
log.error("Database error: %s", e)
|
||||
local_db.session.close()
|
||||
self._handleError(error_message)
|
||||
self._handleError(N_("Oops! Database Error: %(error)s.", error=e))
|
||||
return
|
||||
self.results['path'] = cur_book.path
|
||||
self.title = cur_book.title
|
||||
self.results['title'] = self.title
|
||||
if not config.config_use_google_drive:
|
||||
self._handleSuccess()
|
||||
return os.path.basename(file_path + format_new_ext)
|
||||
local_db.session.close()
|
||||
return os.path.basename(file_path + format_new_ext)
|
||||
else:
|
||||
error_message = N_('%(format)s format not found on disk', format=format_new_ext.upper())
|
||||
local_db.session.close()
|
||||
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
|
||||
book_id,
|
||||
format_new_ext)
|
||||
|
||||
if config.config_kepubifypath and format_old_ext == '.epub' and format_new_ext == '.kepub':
|
||||
check, error_message = self._convert_kepubify(file_path,
|
||||
format_old_ext,
|
||||
format_new_ext)
|
||||
else:
|
||||
# check if calibre converter-executable is existing
|
||||
if not os.path.exists(config.config_converterpath):
|
||||
self._handleError(N_("Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
|
||||
return
|
||||
has_cover = local_db.get_book(book_id).has_cover
|
||||
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext, has_cover)
|
||||
|
||||
if check == 0:
|
||||
cur_book = local_db.get_book(book_id)
|
||||
if os.path.isfile(file_path + format_new_ext):
|
||||
new_format = local_db.session.query(db.Data).filter(db.Data.book == book_id) \
|
||||
.filter(db.Data.format == self.settings['new_book_format'].upper()).one_or_none()
|
||||
if not new_format:
|
||||
new_format = db.Data(name=cur_book.data[0].name,
|
||||
book_format=self.settings['new_book_format'].upper(),
|
||||
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
|
||||
try:
|
||||
local_db.session.merge(new_format)
|
||||
local_db.session.commit()
|
||||
if self.settings['new_book_format'].upper() in ['KEPUB', 'EPUB', 'EPUB3']:
|
||||
ub_session = init_db_thread()
|
||||
remove_synced_book(book_id, True, ub_session)
|
||||
ub_session.close()
|
||||
except SQLAlchemyError as e:
|
||||
local_db.session.rollback()
|
||||
log.error("Database error: %s", e)
|
||||
local_db.session.close()
|
||||
self._handleError(error_message)
|
||||
return
|
||||
self.results['path'] = cur_book.path
|
||||
self.title = cur_book.title
|
||||
self.results['title'] = self.title
|
||||
if not config.config_use_google_drive:
|
||||
self._handleSuccess()
|
||||
return os.path.basename(file_path + format_new_ext)
|
||||
else:
|
||||
error_message = N_('%(format)s format not found on disk', format=format_new_ext.upper())
|
||||
local_db.session.close()
|
||||
log.info("ebook converter failed with error while converting book")
|
||||
if not error_message:
|
||||
error_message = N_('Ebook converter failed with unknown error')
|
||||
@@ -281,6 +285,8 @@ class TaskConvert(CalibreTask):
|
||||
else:
|
||||
error_message = ""
|
||||
for ele in calibre_traceback:
|
||||
ele = ele.decode('utf-8', errors="ignore").strip('\n')
|
||||
log.debug(ele)
|
||||
if not ele.startswith('Traceback') and not ele.startswith(' File'):
|
||||
error_message = N_("Calibre failed with error: %(error)s", error=ele)
|
||||
return check, error_message
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from flask_babel import lazy_gettext as N_
|
||||
|
||||
from cps import config, logger, db, ub
|
||||
from cps import config, logger, db, ub, app
|
||||
from cps.services.worker import CalibreTask
|
||||
|
||||
|
||||
@@ -26,11 +26,13 @@ class TaskReconnectDatabase(CalibreTask):
|
||||
def __init__(self, task_message=N_('Reconnecting Calibre database')):
|
||||
super(TaskReconnectDatabase, self).__init__(task_message)
|
||||
self.log = logger.create()
|
||||
self.calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
# self.calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
|
||||
def run(self, worker_thread):
|
||||
self.calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
self.calibre_db.session.close()
|
||||
with app.app_context():
|
||||
calibre_db = db.CalibreDB(app)
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
# self.calibre_db.session.close()
|
||||
self._handleSuccess()
|
||||
|
||||
@property
|
||||
|
||||
+15
-11
@@ -25,7 +25,7 @@ import mimetypes
|
||||
|
||||
from io import StringIO
|
||||
from email.message import EmailMessage
|
||||
from email.utils import formatdate, parseaddr
|
||||
from email.utils import formatdate, parseaddr, make_msgid
|
||||
from email.generator import Generator
|
||||
from flask_babel import lazy_gettext as N_
|
||||
|
||||
@@ -35,7 +35,7 @@ from cps.embed_helper import do_calibre_export
|
||||
from cps import logger, config
|
||||
from cps import gdriveutils
|
||||
from cps.string_helper import strip_whitespaces
|
||||
import uuid
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
@@ -55,8 +55,8 @@ class EmailBase:
|
||||
return (code, resp)
|
||||
|
||||
def send(self, strg):
|
||||
"""Send `strg' to the server."""
|
||||
log.debug_no_auth('send: {}'.format(strg[:300]))
|
||||
"""Send 'strg' to the server."""
|
||||
log.debug_no_auth('send: {}'.format(strg[:300]), stacklevel=2)
|
||||
if hasattr(self, 'sock') and self.sock:
|
||||
try:
|
||||
if self.transferSize:
|
||||
@@ -102,7 +102,7 @@ class Email(EmailBase, smtplib.SMTP):
|
||||
smtplib.SMTP.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
# Class for sending ssl encrypted email with ability to get current progress, , derived from emailbase class
|
||||
# Class for sending ssl encrypted email with ability to get current progress, derived from emailbase class
|
||||
class EmailSSL(EmailBase, smtplib.SMTP_SSL):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -142,7 +142,7 @@ class TaskEmail(CalibreTask):
|
||||
message['To'] = self.recipient
|
||||
message['Subject'] = self.subject
|
||||
message['Date'] = formatdate(localtime=True)
|
||||
message['Message-Id'] = "{}@{}".format(uuid.uuid4(), self.get_msgid_domain())
|
||||
message['Message-ID'] = make_msgid(domain=self.get_msgid_domain())
|
||||
message.set_content(self.text.encode('UTF-8'), "text", "plain")
|
||||
if self.attachment:
|
||||
data = self._get_attachment(self.filepath, self.attachment)
|
||||
@@ -169,10 +169,14 @@ class TaskEmail(CalibreTask):
|
||||
else:
|
||||
self.send_gmail_email(msg)
|
||||
except MemoryError as e:
|
||||
log.error_or_exception(e, stacklevel=3)
|
||||
log.error_or_exception(e, stacklevel=2)
|
||||
self._handleError('MemoryError sending e-mail: {}'.format(str(e)))
|
||||
except (smtplib.SMTPRecipientsRefused) as e:
|
||||
log.error_or_exception(e, stacklevel=2)
|
||||
self._handleError('Smtplib Error sending e-mail: {}'.format(
|
||||
(list(e.args[0].values())[0][1]).decode('utf-8)').replace("\n", '. ')))
|
||||
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
||||
log.error_or_exception(e, stacklevel=3)
|
||||
log.error_or_exception(e, stacklevel=2)
|
||||
if hasattr(e, "smtp_error"):
|
||||
text = e.smtp_error.decode('utf-8').replace("\n", '. ')
|
||||
elif hasattr(e, "message"):
|
||||
@@ -183,10 +187,10 @@ class TaskEmail(CalibreTask):
|
||||
text = ''
|
||||
self._handleError('Smtplib Error sending e-mail: {}'.format(text))
|
||||
except (socket.error) as e:
|
||||
log.error_or_exception(e, stacklevel=3)
|
||||
log.error_or_exception(e, stacklevel=2)
|
||||
self._handleError('Socket Error sending e-mail: {}'.format(e.strerror))
|
||||
except Exception as ex:
|
||||
log.error_or_exception(ex, stacklevel=3)
|
||||
log.error_or_exception(ex, stacklevel=2)
|
||||
self._handleError('Error sending e-mail: {}'.format(ex))
|
||||
|
||||
def send_standard_email(self, msg):
|
||||
@@ -269,7 +273,7 @@ class TaskEmail(CalibreTask):
|
||||
if config.config_binariesdir and config.config_embed_metadata:
|
||||
os.remove(datafile)
|
||||
except IOError as e:
|
||||
log.error_or_exception(e, stacklevel=3)
|
||||
log.error_or_exception(e, stacklevel=2)
|
||||
log.error('The requested file could not be read. Maybe wrong permissions?')
|
||||
return None
|
||||
return data
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import os
|
||||
from lxml import etree
|
||||
|
||||
from cps import config, db, gdriveutils, logger
|
||||
from cps import config, db, gdriveutils, logger, app
|
||||
from cps.services.worker import CalibreTask
|
||||
from flask_babel import lazy_gettext as N_
|
||||
|
||||
@@ -34,7 +34,7 @@ class TaskBackupMetadata(CalibreTask):
|
||||
task_message=N_('Backing up Metadata')):
|
||||
super(TaskBackupMetadata, self).__init__(task_message)
|
||||
self.log = logger.create()
|
||||
self.calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
# self.calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
self.export_language = export_language
|
||||
self.translated_title = translated_title
|
||||
self.set_dirty = set_dirty
|
||||
@@ -46,47 +46,51 @@ class TaskBackupMetadata(CalibreTask):
|
||||
self.backup_metadata()
|
||||
|
||||
def set_all_books_dirty(self):
|
||||
try:
|
||||
books = self.calibre_db.session.query(db.Books).all()
|
||||
for book in books:
|
||||
self.calibre_db.set_metadata_dirty(book.id)
|
||||
self.calibre_db.session.commit()
|
||||
self._handleSuccess()
|
||||
except Exception as ex:
|
||||
self.log.debug('Error adding book for backup: ' + str(ex))
|
||||
self._handleError('Error adding book for backup: ' + str(ex))
|
||||
self.calibre_db.session.rollback()
|
||||
self.calibre_db.session.close()
|
||||
with app.app_context():
|
||||
calibre_dbb = db.CalibreDB(app)
|
||||
try:
|
||||
books = calibre_dbb.session.query(db.Books).all()
|
||||
for book in books:
|
||||
calibre_dbb.set_metadata_dirty(book.id)
|
||||
calibre_dbb.session.commit()
|
||||
self._handleSuccess()
|
||||
except Exception as ex:
|
||||
self.log.debug('Error adding book for backup: ' + str(ex))
|
||||
self._handleError('Error adding book for backup: ' + str(ex))
|
||||
calibre_dbb.session.rollback()
|
||||
# self.calibre_db.session.close()
|
||||
|
||||
def backup_metadata(self):
|
||||
try:
|
||||
metadata_backup = self.calibre_db.session.query(db.Metadata_Dirtied).all()
|
||||
custom_columns = (self.calibre_db.session.query(db.CustomColumns)
|
||||
.filter(db.CustomColumns.mark_for_delete == 0)
|
||||
.filter(db.CustomColumns.datatype.notin_(db.cc_exceptions))
|
||||
.order_by(db.CustomColumns.label).all())
|
||||
count = len(metadata_backup)
|
||||
i = 0
|
||||
for backup in metadata_backup:
|
||||
book = self.calibre_db.session.query(db.Books).filter(db.Books.id == backup.book).one_or_none()
|
||||
self.calibre_db.session.query(db.Metadata_Dirtied).filter(
|
||||
db.Metadata_Dirtied.book == backup.book).delete()
|
||||
self.calibre_db.session.commit()
|
||||
if book:
|
||||
self.open_metadata(book, custom_columns)
|
||||
else:
|
||||
self.log.error("Book {} not found in database".format(backup.book))
|
||||
i += 1
|
||||
self.progress = (1.0 / count) * i
|
||||
self._handleSuccess()
|
||||
self.calibre_db.session.close()
|
||||
with app.app_context():
|
||||
try:
|
||||
calibre_dbb = db.CalibreDB(app)
|
||||
metadata_backup = calibre_dbb.session.query(db.Metadata_Dirtied).all()
|
||||
custom_columns = (calibre_dbb.session.query(db.CustomColumns)
|
||||
.filter(db.CustomColumns.mark_for_delete == 0)
|
||||
.filter(db.CustomColumns.datatype.notin_(db.cc_exceptions))
|
||||
.order_by(db.CustomColumns.label).all())
|
||||
count = len(metadata_backup)
|
||||
i = 0
|
||||
for backup in metadata_backup:
|
||||
book = calibre_dbb.session.query(db.Books).filter(db.Books.id == backup.book).one_or_none()
|
||||
calibre_dbb.session.query(db.Metadata_Dirtied).filter(
|
||||
db.Metadata_Dirtied.book == backup.book).delete()
|
||||
calibre_dbb.session.commit()
|
||||
if book:
|
||||
self.open_metadata(book, custom_columns)
|
||||
else:
|
||||
self.log.error("Book {} not found in database".format(backup.book))
|
||||
i += 1
|
||||
self.progress = (1.0 / count) * i
|
||||
self._handleSuccess()
|
||||
# self.calibre_db.session.close()
|
||||
|
||||
except Exception as ex:
|
||||
b = "NaN" if not hasattr(book, 'id') else book.id
|
||||
self.log.debug('Error creating metadata backup for book {}: '.format(b) + str(ex))
|
||||
self._handleError('Error creating metadata backup: ' + str(ex))
|
||||
self.calibre_db.session.rollback()
|
||||
self.calibre_db.session.close()
|
||||
except Exception as ex:
|
||||
b = "NaN" if not hasattr(book, 'id') else book.id
|
||||
self.log.debug('Error creating metadata backup for book {}: '.format(b) + str(ex))
|
||||
self._handleError('Error creating metadata backup: ' + str(ex))
|
||||
calibre_dbb.session.rollback()
|
||||
# self.calibre_db.session.close()
|
||||
|
||||
def open_metadata(self, book, custom_columns):
|
||||
# package = self.create_new_metadata_backup(book, custom_columns)
|
||||
|
||||
+61
-55
@@ -23,10 +23,11 @@ from io import BytesIO
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .. import constants
|
||||
from cps import config, db, fs, gdriveutils, logger, ub
|
||||
from cps import config, db, fs, gdriveutils, logger, ub, app
|
||||
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
|
||||
from sqlalchemy import func, text, or_
|
||||
from flask_babel import lazy_gettext as N_
|
||||
|
||||
try:
|
||||
from wand.image import Image
|
||||
use_IM = True
|
||||
@@ -113,9 +114,10 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||
@staticmethod
|
||||
def get_books_with_covers(book_id=-1):
|
||||
filter_exp = (db.Books.id == book_id) if book_id != -1 else True
|
||||
calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
books_cover = calibre_db.session.query(db.Books).filter(db.Books.has_cover == 1).filter(filter_exp).all()
|
||||
calibre_db.session.close()
|
||||
with app.app_context():
|
||||
calibre_db = db.CalibreDB(app) #, expire_on_commit=False, init=True)
|
||||
books_cover = calibre_db.session.query(db.Books).filter(db.Books.has_cover == 1).filter(filter_exp).all()
|
||||
# calibre_db.session.close()
|
||||
return books_cover
|
||||
|
||||
def get_book_cover_thumbnails(self, book_id):
|
||||
@@ -246,7 +248,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
||||
super(TaskGenerateSeriesThumbnails, self).__init__(task_message)
|
||||
self.log = logger.create()
|
||||
self.app_db_session = ub.get_new_session_instance()
|
||||
self.calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
# self.calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
self.cache = fs.FileSystem()
|
||||
self.resolutions = [
|
||||
constants.COVER_THUMBNAIL_SMALL,
|
||||
@@ -254,58 +256,60 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
||||
]
|
||||
|
||||
def run(self, worker_thread):
|
||||
if self.calibre_db.session and use_IM and self.stat != STAT_CANCELLED and self.stat != STAT_ENDED:
|
||||
self.message = 'Scanning Series'
|
||||
all_series = self.get_series_with_four_plus_books()
|
||||
count = len(all_series)
|
||||
with app.app_context():
|
||||
calibre_db = db.CalibreDB(app)
|
||||
if calibre_db.session and use_IM and self.stat != STAT_CANCELLED and self.stat != STAT_ENDED:
|
||||
self.message = 'Scanning Series'
|
||||
all_series = self.get_series_with_four_plus_books(calibre_db)
|
||||
count = len(all_series)
|
||||
|
||||
total_generated = 0
|
||||
for i, series in enumerate(all_series):
|
||||
generated = 0
|
||||
series_thumbnails = self.get_series_thumbnails(series.id)
|
||||
series_books = self.get_series_books(series.id)
|
||||
total_generated = 0
|
||||
for i, series in enumerate(all_series):
|
||||
generated = 0
|
||||
series_thumbnails = self.get_series_thumbnails(series.id)
|
||||
series_books = self.get_series_books(series.id, calibre_db)
|
||||
|
||||
# Generate new thumbnails for missing covers
|
||||
resolutions = list(map(lambda t: t.resolution, series_thumbnails))
|
||||
missing_resolutions = list(set(self.resolutions).difference(resolutions))
|
||||
for resolution in missing_resolutions:
|
||||
generated += 1
|
||||
self.create_series_thumbnail(series, series_books, resolution)
|
||||
|
||||
# Replace outdated or missing thumbnails
|
||||
for thumbnail in series_thumbnails:
|
||||
if any(book.last_modified > thumbnail.generated_at for book in series_books):
|
||||
# Generate new thumbnails for missing covers
|
||||
resolutions = list(map(lambda t: t.resolution, series_thumbnails))
|
||||
missing_resolutions = list(set(self.resolutions).difference(resolutions))
|
||||
for resolution in missing_resolutions:
|
||||
generated += 1
|
||||
self.update_series_thumbnail(series_books, thumbnail)
|
||||
self.create_series_thumbnail(series, series_books, resolution)
|
||||
|
||||
elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS):
|
||||
generated += 1
|
||||
self.update_series_thumbnail(series_books, thumbnail)
|
||||
# Replace outdated or missing thumbnails
|
||||
for thumbnail in series_thumbnails:
|
||||
if any(book.last_modified > thumbnail.generated_at for book in series_books):
|
||||
generated += 1
|
||||
self.update_series_thumbnail(series_books, thumbnail)
|
||||
|
||||
# Increment the progress
|
||||
self.progress = (1.0 / count) * i
|
||||
elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS):
|
||||
generated += 1
|
||||
self.update_series_thumbnail(series_books, thumbnail)
|
||||
|
||||
if generated > 0:
|
||||
total_generated += generated
|
||||
self.message = N_('Generated {0} series thumbnails').format(total_generated)
|
||||
# Increment the progress
|
||||
self.progress = (1.0 / count) * i
|
||||
|
||||
# Check if job has been cancelled or ended
|
||||
if self.stat == STAT_CANCELLED:
|
||||
self.log.info(f'GenerateSeriesThumbnails task has been cancelled.')
|
||||
return
|
||||
if generated > 0:
|
||||
total_generated += generated
|
||||
self.message = N_('Generated {0} series thumbnails').format(total_generated)
|
||||
|
||||
if self.stat == STAT_ENDED:
|
||||
self.log.info(f'GenerateSeriesThumbnails task has been ended.')
|
||||
return
|
||||
# Check if job has been cancelled or ended
|
||||
if self.stat == STAT_CANCELLED:
|
||||
self.log.info(f'GenerateSeriesThumbnails task has been cancelled.')
|
||||
return
|
||||
|
||||
if total_generated == 0:
|
||||
self.self_cleanup = True
|
||||
if self.stat == STAT_ENDED:
|
||||
self.log.info(f'GenerateSeriesThumbnails task has been ended.')
|
||||
return
|
||||
|
||||
self._handleSuccess()
|
||||
self.app_db_session.remove()
|
||||
if total_generated == 0:
|
||||
self.self_cleanup = True
|
||||
|
||||
def get_series_with_four_plus_books(self):
|
||||
return self.calibre_db.session \
|
||||
self._handleSuccess()
|
||||
self.app_db_session.remove()
|
||||
|
||||
def get_series_with_four_plus_books(self, calibre_db):
|
||||
return calibre_db.session \
|
||||
.query(db.Series) \
|
||||
.join(db.books_series_link) \
|
||||
.join(db.Books) \
|
||||
@@ -314,8 +318,8 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
||||
.having(func.count('book_series_link') > 3) \
|
||||
.all()
|
||||
|
||||
def get_series_books(self, series_id):
|
||||
return self.calibre_db.session \
|
||||
def get_series_books(self, series_id, calibre_db):
|
||||
return calibre_db.session \
|
||||
.query(db.Books) \
|
||||
.join(db.books_series_link) \
|
||||
.join(db.Series) \
|
||||
@@ -461,13 +465,15 @@ class TaskClearCoverThumbnailCache(CalibreTask):
|
||||
|
||||
def run(self, worker_thread):
|
||||
if self.app_db_session:
|
||||
if self.book_id == 0: # delete superfluous thumbnails
|
||||
calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
thumbnails = (calibre_db.session.query(ub.Thumbnail)
|
||||
.join(db.Books, ub.Thumbnail.entity_id == db.Books.id, isouter=True)
|
||||
.filter(db.Books.id==None)
|
||||
.all())
|
||||
calibre_db.session.close()
|
||||
# delete superfluous thumbnails
|
||||
if self.book_id == 0:
|
||||
with app.app_context():
|
||||
calibre_db = db.CalibreDB(app)
|
||||
thumbnails = (calibre_db.session.query(ub.Thumbnail)
|
||||
.join(db.Books, ub.Thumbnail.entity_id == db.Books.id, isouter=True)
|
||||
.filter(db.Books.id==None)
|
||||
.all())
|
||||
# calibre_db.session.close()
|
||||
elif self.book_id > 0: # make sure single book is selected
|
||||
thumbnails = self.get_thumbnails_for_book(self.book_id)
|
||||
if self.book_id < 0:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{% extends "basic_layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div>
|
||||
|
||||
<h2 id="title">{{ entry.title }}</h2>
|
||||
<div>
|
||||
{% for author in entry.ordered_authors %}
|
||||
<p>{{ author.name.replace("|",",") }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="cover">
|
||||
<img title="{{ entry.title }}" src="{{ url_for('web.get_cover', book_id=entry.id, resolution='og', c=entry|last_modified) }}"/>
|
||||
</div>
|
||||
|
||||
{% if current_user.role_download() %}
|
||||
{% if entry.data|length %}
|
||||
<div>
|
||||
<h2>Download</h2>
|
||||
{% for format in entry.data %}
|
||||
<p>
|
||||
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}">
|
||||
{{ format.format }} ({{ format.uncompressed_size|filesizeformat }})</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<h2>Details</h2>
|
||||
|
||||
{% if entry.series|length > 0 %}
|
||||
<p>{{ _("Book %(index)s of %(range)s", index=entry.series_index | formatfloat(2), range=(entry.series[0].name)|safe) }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.languages|length > 0 %}
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
{{_('Language')}}: {% for language in entry.languages %}{{language.language_name}}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.identifiers|length > 0 %}
|
||||
<div>
|
||||
<p>
|
||||
<span></span>
|
||||
{% for identifier in entry.identifiers %}
|
||||
<p>{{ identifier.format_type() }}: {{ identifier|escape }}</p>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.publishers|length > 0 %}
|
||||
<div>
|
||||
<p>
|
||||
<span>{{ _('Publisher') }}:
|
||||
<span>{{ entry.publishers[0].name }}</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if (entry.pubdate|string)[:10] != '0101-01-01' %}
|
||||
<div>
|
||||
<p>{{ _('Published') }}: {{ entry.pubdate|formatdate }} </p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
||||
<div>
|
||||
<h2 id="decription">{{ _('Description:') }}</h2>
|
||||
{{ entry.comments[0].text|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "basic_layout.html" %}
|
||||
{% block body %}
|
||||
|
||||
<div class="pagination">
|
||||
<div>
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ (pagination.page - 1)|url_for_other_page }}">« {{_('Previous')}}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ (pagination.page + 1)|url_for_other_page }}">{{_('Next')}} »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if entries|length < 1 %}
|
||||
<p>{{_('No Results Found')}}</p>
|
||||
{% endif %}
|
||||
|
||||
{% for entry in entries %}
|
||||
{% if entry.Books.authors %}
|
||||
{% set author = entry.Books.authors[0].name.replace('|',',')|shortentitle(30) %}
|
||||
{% else %}
|
||||
{% set author = '' %}
|
||||
{% endif %}
|
||||
<a href="{{ url_for('basic.show_book', book_id=entry.Books.id) }}">
|
||||
<p class="listing" title="{{entry.Books.title}}">{{ author }} - {{entry.Books.title|shortentitle}}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ current_user.locale }}">
|
||||
|
||||
<head>
|
||||
<title>{{instance}} | {{title}}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name='viewport' content='initial-scale=1,maximum-scale=5,user-scalable=no' />
|
||||
<link href="{{ url_for('static', filename='css/basic.css') }}" rel="stylesheet" media="screen">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
{% if current_user.is_authenticated or g.allow_anonymous %}
|
||||
<nav>
|
||||
<a href="{{url_for('basic.index')}}">
|
||||
<span><h1>{{_('Home')}}</h1></span>
|
||||
</a>
|
||||
<div class="search">
|
||||
<form role="search" action="{{url_for('basic.index')}}" method="GET">
|
||||
<input type="text" id="query" name="query" placeholder="{{_('Search Library')}}" value="{{ searchterm }}">
|
||||
<span>
|
||||
<button type="submit" id="query_submit">{{_('Search')}}</button>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
{% if not current_user.is_anonymous %}
|
||||
<a href="{{url_for('web.logout')}}">
|
||||
<span>{{_('Logout')}}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="theme">
|
||||
<a href="{{url_for('web.index')}}">
|
||||
<span>{{_('Normal Theme')}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -78,26 +78,6 @@
|
||||
<label for="bookAuthor">{{_('Author')}}</label>
|
||||
<input type="text" class="form-control typeahead" autocomplete="off" name="authors" id="authors" value="{{' & '.join(authors)}}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="comments">{{_('Description')}}</label>
|
||||
<textarea class="form-control" name="comments" id="comments" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{_('Identifiers')}}</label>
|
||||
<table class="table" id="identifier-table">
|
||||
{% for identifier in book.identifiers %}
|
||||
<tr>
|
||||
<td><input type="text" class="form-control" name="identifier-type-{{identifier.type}}" value="{{identifier.type}}" required="required" placeholder="{{_('Identifier Type')}}"></td>
|
||||
<td><input type="text" class="form-control" name="identifier-val-{{identifier.type}}" value="{{identifier.val}}" required="required" placeholder="{{_('Identifier Value')}}"></td>
|
||||
<td><a class="btn btn-default" onclick="removeIdentifierLine(this)">{{_('Remove')}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<a id="add-identifier-line" class="btn btn-default">{{_('Add Identifier')}}</a>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tags">{{_('Tags')}}</label>
|
||||
<input type="text" class="form-control typeahead" autocomplete="off" name="tags" id="tags" value="{% for tag in book.tags %}{{tag.name.strip()}}{% if not loop.last %}, {% endif %}{% endfor %}">
|
||||
@@ -110,21 +90,6 @@
|
||||
<label for="series_index">{{_('Series ID')}}</label>
|
||||
<input type="number" step="0.01" min="0" placeholder="1" class="form-control" name="series_index" id="series_index" value="{{book.series_index|formatfloat(2)}}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rating">{{_('Rating')}}</label>
|
||||
<input type="number" name="rating" id="rating" class="rating input-lg" data-clearable="" value="{% if book.ratings %}{{(book.ratings[0].rating / 2)|int}}{% endif %}">
|
||||
</div>
|
||||
{% if current_user.role_upload() and g.allow_upload %}
|
||||
<div class="form-group">
|
||||
<label for="cover_url">{{_('Fetch Cover from URL (JPEG - Image will be downloaded and stored in database)')}}</label>
|
||||
<input type="text" class="form-control" name="cover_url" id="cover_url" value="">
|
||||
</div>
|
||||
<div class="form-group" aria-label="Upload cover from local drive">
|
||||
<label class="btn btn-primary btn-file" for="btn-upload-cover">{{ _('Upload Cover from Local Disk') }}</label>
|
||||
<div class="upload-cover-input-text" id="upload-cover"></div>
|
||||
<input id="btn-upload-cover" name="btn-upload-cover" type="file" accept=".jpg, .jpeg, .png, .webp">
|
||||
</div>
|
||||
{% endif %}
|
||||
<label for="pubdate">{{_('Published Date')}}</label>
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="datepicker form-control" name="pubdate" id="pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdateinput}}{% endif %}">
|
||||
@@ -141,6 +106,39 @@
|
||||
<label for="languages">{{_('Language')}}</label>
|
||||
<input type="text" class="form-control typeahead" autocomplete="off" name="languages" id="languages" value="{% for language in book.languages %}{{language.language_name.strip()}}{% if not loop.last %}, {% endif %}{% endfor %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rating">{{_('Rating')}}</label>
|
||||
<input type="number" name="rating" id="rating" class="rating input-lg" data-clearable="" value="{% if book.ratings %}{{(book.ratings[0].rating / 2)|int}}{% endif %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comments">{{_('Description')}}</label>
|
||||
<textarea class="form-control" name="comments" id="comments" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{_('Identifiers')}}</label>
|
||||
<table class="table" id="identifier-table"><tbody>
|
||||
{% for identifier in book.identifiers %}
|
||||
<tr>
|
||||
<td><input type="text" class="form-control" name="identifier-type-{{identifier.type}}" value="{{identifier.type}}" required="required" placeholder="{{_('Identifier Type')}}"></td>
|
||||
<td><input type="text" class="form-control" name="identifier-val-{{identifier.type}}" value="{{identifier.val}}" required="required" placeholder="{{_('Identifier Value')}}"></td>
|
||||
<td><a class="btn btn-default" onclick="removeIdentifierLine(this)">{{_('Remove')}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<a id="add-identifier-line" class="btn btn-default">{{_('Add Identifier')}}</a>
|
||||
</div>
|
||||
{% if current_user.role_upload() and g.allow_upload %}
|
||||
<div class="form-group">
|
||||
<label for="cover_url">{{_('Fetch Cover from URL (JPEG - Image will be downloaded and stored in database)')}}</label>
|
||||
<input type="text" class="form-control" name="cover_url" id="cover_url" value="">
|
||||
</div>
|
||||
<div class="form-group" aria-label="Upload cover from local drive">
|
||||
<label class="btn btn-primary btn-file" for="btn-upload-cover">{{ _('Upload Cover from Local Disk') }}</label>
|
||||
<div class="upload-cover-input-text" id="upload-cover"></div>
|
||||
<input id="btn-upload-cover" name="btn-upload-cover" type="file" accept=".jpg, .jpeg, .png, .webp">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cc|length > 0 %}
|
||||
{% for c in cc %}
|
||||
<div class="form-group">
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="col-sm-12">
|
||||
<div id="db_submit" name="submit" class="btn btn-default">{{_('Save')}}</div>
|
||||
<button id="db_submit" type="submit" class="btn btn-default">{{_('Save')}}</button>
|
||||
<a href="{{ url_for('admin.admin') }}" id="config_back" class="btn btn-default">{{_('Cancel')}}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -411,7 +411,7 @@
|
||||
</div>
|
||||
<div class="form-group" style="margin-left:10px;">
|
||||
<input type="checkbox" id="config_password_character" name="config_password_character" {% if config.config_password_character %}checked{% endif %}>
|
||||
<label for="config_password_lower">{{_('Enforce characters (needed For Chinese/Japanese/Korean Characters)')}}</label>
|
||||
<label for="config_password_character">{{_('Enforce characters (needed For Chinese/Japanese/Korean Characters)')}}</label>
|
||||
</div>
|
||||
<div class="form-group" style="margin-left:10px;">
|
||||
<input type="checkbox" id="config_password_special" name="config_password_special" {% if config.config_password_special %}checked{% endif %}>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{% extends is_xhr|yesno("fragment.html", "layout.html") %}
|
||||
{% block header %}
|
||||
<meta property="og:type" content="book" />
|
||||
<meta property="og:title" content="{{ entry.title|truncate(35) }}" />
|
||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
||||
<meta property="og:description" content="{{ entry.comments[0].text|striptags|truncate(65) }}" />
|
||||
<meta property="og:image" content="{{url_for('web.get_cover', book_id=entry.id, resolution='og', c=entry|last_modified)}}" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div class="single">
|
||||
<div class="row">
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{% endif %}
|
||||
{% for format in entry.Books.data %}
|
||||
<link rel="http://opds-spec.org/acquisition" href="{{ url_for('opds.opds_download_link', book_id=entry.Books.id, book_format=format.format|lower)}}"
|
||||
length="{{format.uncompressed_size}}" mtime="{{entry.Books.atom_timestamp}}" type="{{format.format|lower|mimetype}}"/>
|
||||
length="{{format.uncompressed_size}}" title="{{format.format}}" mtime="{{entry.Books.atom_timestamp}}" type="{{format.format|lower|mimetype}}"/>
|
||||
{% endfor %}
|
||||
</entry>
|
||||
{% endfor %}
|
||||
|
||||
@@ -48,7 +48,11 @@
|
||||
<div class="row">
|
||||
<div class="col errorlink">
|
||||
{% if not unconfigured %}
|
||||
<a href="{{url_for('web.index')}}" title="{{ _('Return to Home') }}">{{_('Return to Home')}}</a>
|
||||
{% if goto_admin %}
|
||||
<a href="{{url_for('admin.db_configuration')}}" title="{{ _('Return to Database config') }}">{{_('Return to Database config')}}</a>
|
||||
{% else %}
|
||||
<a href="{{url_for('web.index')}}" title="{{ _('Return to Home') }}">{{_('Return to Home')}}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{{url_for('web.logout')}}" title="{{ _('Logout User') }}">{{ _('Logout User') }}</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
srcset="{{ srcset }}"
|
||||
src="{{ url_for('web.get_cover', book_id=book.id, resolution='og', c=book|last_modified) }}"
|
||||
alt="{{ image_alt }}"
|
||||
loading="lazy"
|
||||
/>
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -16,5 +17,6 @@
|
||||
srcset="{{ srcset }}"
|
||||
src="{{ url_for('web.get_series_cover', series_id=series.id, resolution='og', c='day'|cache_timestamp) }}"
|
||||
alt="{{ title }}"
|
||||
loading="lazy"
|
||||
/>
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -37,11 +37,12 @@
|
||||
<a class="navbar-brand" href="{{url_for('web.index')}}">{{instance}}</a>
|
||||
</div>
|
||||
{% if g.current_theme == 1 %}
|
||||
<div class="home-btn"><a class="home-btn-tooltip" href="{{url_for("web.index",page=1)}}" data-toggle="tooltip" title="" data-placement="bottom" data-original-title="Home"></a></div>
|
||||
<div class="home-btn"><a class="home-btn-tooltip" href="{{url_for("web.index", page=1)}}" data-toggle="tooltip" title="" data-placement="bottom" data-original-title="Home"></a></div>
|
||||
<div class="plexBack"><a href="{{url_for('web.index')}}"></a></div>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated or g.allow_anonymous %}
|
||||
<form class="navbar-form navbar-left" role="search" action="{{url_for('search.simple_search')}}" method="GET">
|
||||
<!--# margin 0, padding 15, background color-->
|
||||
<form class="navbar-form navbar-left" role="search" action="{{url_for('search.simple_search')}}" method="GET">
|
||||
<div class="form-group input-group input-group-sm">
|
||||
<label for="query" class="sr-only">{{_('Search')}}</label>
|
||||
<input type="text" class="form-control" id="query" name="query" placeholder="{{_('Search Library')}}" value="{{searchterm}}">
|
||||
@@ -55,6 +56,7 @@
|
||||
{% if current_user.is_authenticated or g.allow_anonymous %}
|
||||
<ul class="nav navbar-nav ">
|
||||
<li><a href="{{url_for('search.advanced_search')}}" id="advanced_search"><span class="glyphicon glyphicon-search"></span><span class="hidden-sm"> {{_('Advanced Search')}}</span></a></li>
|
||||
{% if simple==true %} <li><a href="{{url_for('basic.index')}}" id="basic"><span class="glyphicon glyphicon-phone"></span><span>{{_('Simple Theme')}}</span></a><li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="nav navbar-nav navbar-right" id="main-nav">
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="md-content">
|
||||
<h3>{{_('Settings')}}</h3>
|
||||
<div class="form-group themes" id="themes">
|
||||
Choose a theme below: <br />
|
||||
{{_('Choose a theme below:')}}}<br />
|
||||
|
||||
<!-- Hardcoded a tick in the light theme button because it is the "default" theme. Need to find a way to do this dynamically on startup-->
|
||||
<button type="button" id="lightTheme" class="lightTheme" onclick="selectTheme(this.id)"><span
|
||||
@@ -126,8 +126,8 @@
|
||||
window.calibre = {
|
||||
filePath: "{{ url_for('static', filename='js/libs/') }}",
|
||||
cssPath: "{{ url_for('static', filename='css/') }}",
|
||||
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=bookid, book_format='EPUB') }}",
|
||||
bookUrl: "{{ url_for('web.serve_book', book_id=bookid, book_format='epub', anyname='file.epub') }}",
|
||||
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=bookid, book_format=book_format) }}",
|
||||
bookUrl: "{{ url_for('web.serve_book', book_id=bookid, book_format=book_format, anyname='file.epub') }}",
|
||||
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
|
||||
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
|
||||
};
|
||||
@@ -175,6 +175,7 @@
|
||||
// Apply theme to rest of the page.
|
||||
document.getElementById("main").style.backgroundColor = themes[id]["bgColor"];
|
||||
document.getElementById("titlebar").style.color = themes[id]["title-color"] || "#fff";
|
||||
document.getElementById("progress").style.color = themes[id]["title-color"] || "#fff";
|
||||
}
|
||||
|
||||
// font size settings logic
|
||||
@@ -215,6 +216,6 @@
|
||||
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/reading/epub.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/reading/epub-progress.js') }}"></script>
|
||||
<!--script src="{{ url_for('static', filename='js/reading/locationchange-polyfill.js') }}"></script-->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -35,7 +35,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/viewer.css') }}">
|
||||
<!-- This snippet is used in production (included from viewer.html) -->
|
||||
<link rel="resource" type="application/l10n" href="{{ url_for('static', filename='locale/locale.json') }}">
|
||||
<script src="{{ url_for('static', filename='js/libs/pdf.mjs') }}" type="module"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/pdf.js') }}" type="module"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('webviewerloaded', function() {
|
||||
@@ -46,12 +46,12 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
PDFViewerApplicationOptions.set('cMapUrl', "{{ url_for('static', filename='cmaps/') }}");
|
||||
PDFViewerApplicationOptions.set('sidebarViewOnLoad', 0);
|
||||
PDFViewerApplicationOptions.set('imageResourcesPath', "{{ url_for('static', filename='css/images/') }}");
|
||||
PDFViewerApplicationOptions.set('workerSrc', "{{ url_for('static', filename='js/libs/pdf.worker.mjs') }}");
|
||||
PDFViewerApplicationOptions.set('workerSrc', "{{ url_for('static', filename='js/libs/pdf.worker.js') }}");
|
||||
PDFViewerApplicationOptions.set('defaultUrl',"{{ url_for('web.serve_book', book_id=pdffile, book_format='pdf') }}")
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/libs/viewer.mjs') }}" type="module">></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/viewer.js') }}" type="module">></script>
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1021
-1049
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
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