Compare commits

..
238 Commits
Author SHA1 Message Date
Ozzie Isaacs 1451a67912 Version bump 2023-03-27 19:49:57 +02:00
Ozzie Isaacs a72f0a160b Bugfix for python3.11, table gdrive_ids2 already exists (fix for #2729) 2023-03-27 17:58:07 +02:00
Ozzie Isaacs 253386b0a5 Bugfix parse ldap server config 2023-03-26 18:27:43 +02:00
Ozzie Isaacs 6c8ffb3e7e Bugfix path selection for reverse proxy 2023-03-26 14:31:19 +02:00
Ozzie Isaacs 7d26e6fc85 Bugfix get updater status 2023-03-26 14:19:12 +02:00
Ozzie Isaacs 085a6b88a3 Fix for #2713 (strip scheme from ldap server name) 2023-03-26 14:08:09 +02:00
Ozzie Isaacs bde36e3cd4 Bugfix for logging ldap debug messages with non stream logfile 2023-03-26 13:17:02 +02:00
Ozzie Isaacs 9646b6e2dd Enable debug output for ldap login 2023-03-26 11:29:54 +02:00
Ozzie Isaacs d35e781d41 Bugfixes after testrun
Catch StaleDataError
2023-03-26 08:19:36 +02:00
Ozzie Isaacs 321db4d712 Refactored send email by make use of ajax calls instead of posting the page
Always use getPath instead of pathname
2023-03-25 12:34:16 +01:00
Ozzie Isaacs 2b9f920454 Metadata backup configurable 2023-03-25 10:42:36 +01:00
Ozzie Isaacs 45acd3febe Merge branch jvonau/patch-1
Updated requirements
Updated test results
2023-03-22 17:50:38 +01:00
Ozzie Isaacs cbd7ca2f3e Activate metadata backup 2023-03-21 20:03:45 +01:00
Ozzie Isaacs ba7fee3918 Update metadatabackup 2023-03-21 20:02:57 +01:00
Ozzie Isaacs 1210ccb43f Update teststatus 2023-03-21 18:51:56 +01:00
Jerry Vonau 04f1f6493b Update optional-requirements.txt
faust-cchardet has the latest code for cchardet https://pypi.org/project/faust-cchardet/#history
2023-03-21 09:52:38 -05:00
Ozzie Isaacs 46d2d217ee Bugfix metadata backup with custom columns 2023-03-20 20:11:37 +01:00
Ozzie Isaacs e3fffa8a8f Bugfix backup metadata for custom date, custom categories 2023-03-20 19:57:05 +01:00
Ozzie Isaacs dfb49bfca9 Merge branch 'master' into Develop (handle case of cover smaller than thumbnail) 2023-03-20 19:03:15 +01:00
Ozzie Isaacs 224777f5e3 Handle case that cover size is smaller than thumbnail size
Update teststatus
2023-03-20 19:00:35 +01:00
Ozzie Isaacs 7ade4615a4 Bugfix write metadata
Bugfix change of custom column now sets updates the book timestamp
2023-03-19 17:23:05 +01:00
Ozzie Isaacs cbd679eb24 Bugfix write metadata
Bugfix change of custom column now sets updates the book timestamp
2023-03-19 17:21:30 +01:00
Ozzie Isaacs b277ed3359 Update teststatus 2023-03-19 15:29:23 +01:00
Ozzie Isaacs fa95b07a95 Merge branch 'master' into Develop
# Conflicts:
#	requirements.txt
2023-03-05 16:06:49 +01:00
Ozzie Isaacs 87bc8c6d96 Updated requirements 2023-03-05 16:06:14 +01:00
Ozzie Isaacs db2bc6a2c2 Changed requirements 2023-03-05 16:03:54 +01:00
Ozzie Isaacs cf850c6ed5 Bugfix backup metadata 2023-03-05 16:03:07 +01:00
Ozzie Isaacs a6b54e398b Bugfix backup metadata 2023-03-05 16:02:48 +01:00
Ozzie Isaacs 28eeb9eec3 Merge branch 'master' into Develop 2023-03-04 11:13:57 +01:00
Ozzie Isaacs 7d76f2ae33 Updated requirements 2023-03-04 11:13:41 +01:00
Ozzie Isaacs 49e4f540c9 ** Be careful, after updating, there is no way back **
** Please install flask-limiter after updating **

Update Teststatus
Bugfix after merge
Bugfix generate Metadata backup
2023-03-04 10:37:05 +01:00
Ozzie Isaacs 64e9b13311 Bugfix after merge
Bugfix generate Metadata backup
2023-03-03 19:59:19 +01:00
Ozzie Isaacs 3cf778b591 Prevent double error message logging in case error in delete user 2023-02-27 19:27:48 +01:00
Ozzie Isaacs 942bcff5c4 Merge branch 'Develop' 2023-02-27 18:54:32 +01:00
Ozzie Isaacs 5c5db34a52 Merge branch 'master' into Develop
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2023-02-27 18:54:02 +01:00
Ozzie Isaacs ae850172a3 Deactivate metadata backup 2023-02-27 18:53:46 +01:00
Ozzie Isaacs 7ff4747f63 Bugfix after merge 2023-02-27 13:17:34 +01:00
Ozzie Isaacs 76b0411c33 Bugfix failed tasks can no longer aborted
Metdatabackup is done on startup if app mode is test
2023-02-25 16:31:48 +01:00
Ozzie Isaacs a414db0243 Merge remote-tracking branch 'fixed_layout/kobo-sync-detect-fixed-layout'
# Conflicts:
#	cps/kobo.py
2023-02-25 15:26:49 +01:00
Ozzie Isaacs 162ac73bee Bugfixes from testrun 2023-02-22 18:59:11 +01:00
Ozzie Isaacs fc31132f4e Merge remote-tracking branch 'pdf/master' 2023-02-21 20:52:25 +01:00
Ozzie Isaacs 856dce8add Merge remote-tracking branch 'refactor_epub/refactor_epub'
# Conflicts:
#	cps/epub.py
2023-02-21 20:47:10 +01:00
Ozzie Isaacs 3debe4aa4b Merge remote-tracking branch 'comic/long_strip_cbrreader' 2023-02-21 20:33:11 +01:00
Ozzie Isaacs b28a2cc58c Merge branch 'master' into Develop
# Conflicts:
#	cps/web.py
#	test/Calibre-Web TestSummary_Linux.html
2023-02-21 17:03:54 +01:00
Ozzie Isaacs 5fd0e4c046 Updated testresult 2023-02-20 18:46:48 +01:00
Ozzie Isaacs 595f01e7a3 Bugfix change erader email in /me page 2023-02-19 19:36:52 +01:00
Ozzie Isaacs c79aa75f00 Merge remote-tracking branch 'author_opds/opds_add_metadata' 2023-02-19 18:34:40 +01:00
Ozzie Isaacs 0177a8bcca Update installing problem on Raspberry Pi 2023-02-19 16:03:25 +01:00
Ozzie Isaacs 38c601bb10 Bugfix server restart to prevent infinite calibre-web instances 2023-02-19 09:10:25 +01:00
Ozzie Isaacs e7a6fe0bec Bugfix restart calibre-web on windows 2023-02-19 09:03:01 +01:00
Ozzie Isaacs 6119eb3681 Revert restart change 2023-02-18 14:34:53 +01:00
Ozzie Isaacs 6b2ca9537d revert restart change 2023-02-18 14:34:31 +01:00
Ozzie Isaacs 3cb9a9b04a Merge branch 'master' into Develop
# Conflicts:
#	cps/server.py
2023-02-18 14:23:58 +01:00
Ozzie Isaacs 3d8256b6a6 Bugfix for restarting on ubuntu 20.04 on restart 2023-02-18 14:23:15 +01:00
Ozzie Isaacs 660d1fb1ff Fix for infinite creation of subprocesses on restart 2023-02-18 13:59:10 +01:00
Ozzie Isaacs fa3fe47059 Fix for infinite process generation on restart 2023-02-18 13:58:22 +01:00
Ozzie Isaacs 89bc72958e new random password generation algorithm to ensure compliance with password rules
bugfix opds login limit
2023-02-16 16:23:06 +01:00
Ozzie Isaacs 73ea18b8ce Update teststatus 2023-02-16 13:08:39 +01:00
Ozzie Isaacs 7ca07f06ce Bugfix change password on commandline 2023-02-15 20:17:10 +01:00
Ozzie Isaacs 8ee34bf428 Bugfixes for password policy 2023-02-15 19:53:35 +01:00
Ozzie Isaacs 66d5b5a697 Exclude also .key file from updater 2023-02-12 13:12:38 +01:00
Ozzie Isaacs ce48e06c45 Improved limiter 2023-02-12 13:10:00 +01:00
Ozzie Isaacs f4ecfe4aca Merge branch 'master' into Develop
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2023-02-11 07:44:40 +01:00
Ozzie Isaacs dda20eb912 Further improvements for sqlalchemy compatibility 2023-02-11 07:43:48 +01:00
GarcaMan c4326c9495 Merge branch 'master' of github.com:GarckaMan/calibre-web into long_strip_cbrreader 2023-02-11 03:25:31 +00:00
Ozzie Isaacs 63a3edd429 Merge remote-tracking branch 'csp/patch-2'
Updated testresult
2023-02-10 18:18:27 +01:00
Ozzie Isaacs 3b45234beb Bugfix from testrun 2023-02-09 19:46:36 +01:00
Ozzie Isaacs 8d0a699078 Merge branch 'master' into Develop 2023-02-07 18:38:47 +01:00
Ozzie Isaacs 5b5146a793 Merge remote-tracking branch 'csp/patch-2' 2023-02-07 18:38:25 +01:00
Ozzie Isaacs 7a4e6fbdfb Merge branch 'master' into Develop
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2023-02-06 19:02:47 +01:00
Ozzie Isaacs 14d14637cd Updated test status
updated jzip for epub reader
Bugfix for opds login with ldap
updated requirementes
2023-02-06 19:02:27 +01:00
Ozzie Isaacs fb42f6bfff Make it possible to disable ratelimiter
Update APScheduler
Error message on missing flask-limiter
2023-02-05 13:43:35 +01:00
Ozzie Isaacs 4b7a0f3662 Merge branch 'master' into Develop
# Conflicts:
#	cps/opds.py
#	cps/server.py
#	cps/web.py
2023-02-05 12:10:01 +01:00
Ozzie Isaacs 275675b48a Search query is now also a post request (possible fix for Forward Auth Search Redirect Issue #2681) 2023-02-05 09:34:57 +01:00
Ozzie Isaacs 907606295d Merge remote-tracking branch 'it/patch-1' 2023-02-05 08:50:33 +01:00
Ozzie Isaacs 794c6ba254 Updated chinese translation 2023-02-05 08:47:10 +01:00
Ozzie Isaacs ac13f6042a Removed prints
Enabled additional reverse proxy authentication for opds feeds (fixes #2399)
2023-02-05 08:47:10 +01:00
Ozzie Isaacs f8fbc807f1 further refactored user login 2023-02-05 08:47:10 +01:00
Ozzie Isaacs 98da7dd5b0 remove g.user from before request 2023-02-05 08:47:10 +01:00
Ozzie Isaacs 1c3b69c710 refactored login routines 2023-02-05 08:47:10 +01:00
mapi68 1dd638a786 Update messages.po 2023-02-04 20:39:36 +01:00
_Fervor_ 6da7d05c6c Update readpdf.html 2023-02-03 23:34:55 +08:00
_Fervor_ 3f72c3fffe Update web.py 2023-02-03 23:31:49 +08:00
Ozzie Isaacs cf9a7d538f Add .key file to ignored files 2023-02-03 14:28:59 +01:00
Ozzie Isaacs ea9e8d4384 Delete .key 2023-02-03 14:27:59 +01:00
Ozzie Isaacs b9769a0975 Revert to latest syncronous jszip version to make comic reader work again 2023-02-01 18:46:23 +01:00
Ozzie Isaacs 3a262661b5 Update test status 2023-01-31 18:54:20 +01:00
Ozzie Isaacs d2056ceb51 Updated readme to make
and exclude library from getting zipped
2023-01-29 14:28:49 +01:00
Ozzie Isaacs e71a3452e1 Added empty database 2023-01-29 14:14:18 +01:00
Ozzie Isaacs 189da65fac leave fields filled after invalid login attempt 2023-01-29 13:20:22 +01:00
Ozzie Isaacs 0e6b7f96d3 Update flask requirement 2023-01-29 10:01:30 +01:00
Ozzie Isaacs 1babb566fb Update version 2023-01-29 09:55:32 +01:00
Ozzie Isaacs c4e4acfc26 Stop scheduler also on restart calibre-web 2023-01-29 09:54:07 +01:00
Ozzie Isaacs 6afb429185 Stop Scheduler also on reboot 2023-01-29 09:53:02 +01:00
Ozzie Isaacs f241b260d7 Updated requirements
Bugfix from testrun
Testresults
2023-01-29 09:52:25 +01:00
Ozzie Isaacs 260a694834 Bugfixes after merge 2023-01-28 18:59:14 +01:00
Ozzie Isaacs 508e2b4d0a Merge branch 'master' into Develop
# Conflicts:
#	cps/admin.py
#	cps/config_sql.py
#	cps/search.py
#	cps/templates/admin.html
#	cps/web.py
#	setup.cfg
#	test/Calibre-Web TestSummary_Linux.html
2023-01-28 18:52:50 +01:00
Ozzie Isaacs 9701a97a57 Updated optional-requirements 2023-01-28 18:42:20 +01:00
Ozzie Isaacs 4913f06e0d Updated test status
Fix for #2614 (Send to eReader not working for guest user)
2023-01-24 18:07:21 +01:00
Petipopotam d545ea9e6f CSP invalid to display image when web.read_book
CSP 
Before : default-src 'self'  'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:; style-src-elem 'self' blob: 'unsafe-inline'; object-src 'none';
After :    default-src 'self'  'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data: blob:; style-src-elem 'self' blob: 'unsafe-inline';object-src 'none';
2023-01-24 11:03:19 +01:00
Petipopotam 1ad8dc102a CSP invalid syntax
CSP had some "cosmetic" errors

Before : default-src 'self'  'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data: style-src-elem 'self' blob: 'unsafe-inline';object-src: 'none';
After :    default-src 'self'  'unsafe-inline' 'unsafe-eval'; font-src 'self' data:;  img-src 'self' data:; style-src-elem 'self' blob: 'unsafe-inline'; object-src 'none';
2023-01-24 10:51:48 +01:00
Ozzie Isaacs 36cb454d1c Bugfixes from testrun 2023-01-23 16:04:25 +01:00
Ozzie Isaacs 1899cda8d1 Update Teststatus 2023-01-23 12:55:18 +01:00
Ozzie Isaacs 8dd4d0be1b Merge handle epub iodentifier 2023-01-23 12:55:09 +01:00
Ozzie Isaacs d48d6880af Update German translation 2023-01-22 13:53:10 +01:00
Ozzie Isaacs 94a6931d48 Handle version 3.0 of flask-babel 2023-01-22 12:09:19 +01:00
Ozzie Isaacs c21a870b8e Migrated pypdf2 to the now active developed pypdf 2023-01-22 11:31:47 +01:00
Ozzie Isaacs 791bc9621a Improved parsing of pdf files, bugfix for pypdf2 > V3.0 2023-01-22 11:25:24 +01:00
Ozzie Isaacs 2d6fe483ba Fix for #2657 (TypeError: 'NoneType' object is not iterable from amazon) 2023-01-22 08:02:17 +01:00
Ozzie Isaacs ad43f07dab Added additional installation hints based on #2660 2023-01-22 07:52:17 +01:00
Ozzie Isaacs 77637d81dd Fix fro #2670 (user has no attribute eReader_mail) 2023-01-22 07:42:44 +01:00
Ozzie Isaacs a2bf6dfb7b Bugfix csp header
Bugfix for loading metadata from google with old books (publishing date only year)
2023-01-21 17:09:02 +01:00
Ozzie Isaacs 1cd05d614c Merge remote-tracking branch 'csp/patch-1' 2023-01-21 15:48:08 +01:00
Ozzie Isaacs d75f681247 Merge remote-tracking branch 'no/Translate-to-norwegian' 2023-01-21 15:44:10 +01:00
Ozzie Isaacs 2be2920833 Fixed typo 2023-01-21 15:27:51 +01:00
Ozzie Isaacs d6184619f5 New generated translation files 2023-01-21 15:27:11 +01:00
Ozzie Isaacs 43ee85fbb5 Removed unnecessary Unicode "u" 2023-01-21 15:23:18 +01:00
Ozzie Isaacs 8022b1bb36 Merge remote-tracking branch 'english/master' 2023-01-21 15:19:59 +01:00
Ozzie Isaacs 9e75c65af8 Merge remote-tracking branch 'pdfreader/issue-2659' 2023-01-21 14:27:59 +01:00
Ozzie Isaacs 7881950e66 Merge remote-tracking branch 'id-translation/master' 2023-01-21 14:18:45 +01:00
Ozzie Isaacs 031658ae94 Update teststatus 2023-01-21 14:17:43 +01:00
Arief Hidayat 48c2c7b543 First fix after proofread
Fixed typos and inconsistencies.
Defined "Berkas" instead of "File" for English term "File".
Defined "Pengaturan" and its root word "Atur" instead of "Konfigurasi" for English term "Configuration".
Reverting technical terms "Logfile", "access logfile", "Keyfile" to its English origin.
2023-01-21 09:37:16 +07:00
Petipopotam beb619c2c2 Correct CSP
no need blob: value for object-src
2023-01-19 20:19:55 +01:00
Petipopotam ed22209e6c Content Security Policy syntax was invalid
According to https://csp-evaluator.withgoogle.com/ the CSP built here is NOT valid (and the blob: value is missing at img-src, so the image is not displayed when reading ebook in a browser)

Before this commit, in Chrome response header you can find 

Content-Security-Policy: default-src 'self'  'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self'  data:; object-src: 'none'; blob:;style-src-elem 'self' blob: 'unsafe-inline';

After :

Content-Security-Policy: default-src 'self'  'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' blob: data:; object-src 'none'  blob:; style-src-elem 'self' blob: 'unsafe-inline';

and image in viewer are displayed
2023-01-19 19:56:27 +01:00
blitzmann 364c48edd8 PdfFileReader -> PdfReader 2023-01-16 22:59:26 -05:00
Ozzie Isaacs e178efb58c Update for #2653 (AP Scheduler triggers are function calls and not strings anymore) 2023-01-15 13:49:16 +01:00
Vegard Fladby 4105c64320 Added norwegian translation 2023-01-06 10:17:53 +01:00
Josh O'Brien b3335f6733 English Language Updates - V3 2023-01-04 13:30:13 +11:00
Benedikt McMullin fba95956de epub: Skip invalid dc:identifier 2023-01-02 21:21:43 +01:00
Ozzie Isaacs ce0b3d8d10 Merge remote-tracking branch 'bump_sortable' 2022-12-27 09:13:15 +01:00
Ozzie Isaacs 9545aa2a0b Merge remote-tracking branch 'kobo_path/master' 2022-12-27 09:11:08 +01:00
jvoisin 4629eec774 Bump sortable.js 2022-12-27 00:18:56 +01:00
Jeroen Kroese 4977381b1c Fix 'Kobo eReader.conf' path 2022-12-26 16:19:19 +01:00
Ozzie Isaacs 6c1631acba Updated requirements 2022-12-26 15:47:04 +01:00
Ozzie Isaacs 1489228649 Bugfix for testability 2022-12-25 19:06:35 +01:00
Ozzie Isaacs 74efa52f26 Update lxml, pypdf2 requirement 2022-12-25 18:48:26 +01:00
Ozzie Isaacs 1ca1281346 Merge remote-tracking branch 'jszip/compress_dl' 2022-12-25 18:38:28 +01:00
jvoisin 631496775e Minor code refactorisation of epub.py
- Reduce the amount of nested indentation
- Use proper functions instead of fragile manual parsing
2022-12-25 16:37:58 +01:00
jvoisin c5e539bbcd Bump jszip 2022-12-25 16:11:29 +01:00
jvoisin 02ec853e3b Remove a duplicate library 2022-12-25 16:09:18 +01:00
Ozzie Isaacs d0411fd9c7 Merge remote-tracking branch 'google_cover/patch-1' 2022-12-25 13:27:05 +01:00
Ozzie Isaacs 567cb2e097 Inlcuded indonesian translation 2022-12-25 13:24:00 +01:00
Ozzie Isaacs a635e136be Added databazeknih to supported links 2022-12-25 13:10:21 +01:00
Ozzie Isaacs 5ffb3e917f Merge remote-tracking branch 'caliblur/issue/2515' 2022-12-25 11:04:14 +01:00
Ozzie Isaacs 5dc3385ae5 Merge remote-tracking branch 'douban/metadata_provider/douban' 2022-12-25 10:35:35 +01:00
Ozzie Isaacs 66e0a81d23 Merge remote-tracking branch 'mp3_csrf/save-mp3-position' 2022-12-25 10:34:01 +01:00
Ozzie Isaacs 1f6eb2def6 html tag fix 2022-12-25 10:32:01 +01:00
Ozzie Isaacs 7d3af5bbd0 Merge remote-tracking branch 'font_size/issue/487' 2022-12-25 10:29:29 +01:00
Ozzie Isaacs 043a612d1a Update path to kobo config file 2022-12-25 10:22:58 +01:00
Ozzie Isaacs 928e24fd1a Merge remote-tracking branch 'google_site_verification/master' 2022-12-25 09:59:32 +01:00
Ozzie Isaacs 3361c41c6d Merge remote-tracking branch 'caliblur/issues/caliBlur' 2022-12-25 09:49:02 +01:00
Ozzie Isaacs 85a6616606 Merge remote-tracking branch 'fix_default_language/master' 2022-12-25 09:45:14 +01:00
Ozzie Isaacs c15b603fef Merge remote-tracking branch 'mp3listen/master' 2022-12-25 09:41:56 +01:00
Ozzie Isaacs b12e47d0e5 Merge remote-tracking branch 'fr' 2022-12-25 09:29:05 +01:00
Ozzie Isaacs 389263f5e7 Merge remote-tracking branch 'object_src_csp' 2022-12-25 09:27:48 +01:00
Ozzie Isaacs 307b4526f6 Merge remote-tracking branch 'jquery/bump_jquery' 2022-12-25 09:23:18 +01:00
jvoisin 7d023ce741 Bump jquery's version from 3.6.0 to 3.6.3 2022-12-22 23:38:28 +01:00
Julien Voisin 2ddbaa2150 Add object-src to the CSP policy 2022-12-22 12:47:37 +01:00
jvoisin 29fef4a314 Add French articles to the title regex 2022-12-20 23:14:41 +01:00
JonathanHerrewijnen 9450084d6e Editing listenmp3.html page to show audiobook info 2022-11-24 20:34:01 +00:00
Vijay Pillai b52c7aac53 Added GOOGLE_SITE_VERIFICATION environmental value to enable google safe browsing 2022-11-10 11:02:50 -05:00
Feige-cn e8c461b14f Update web.py
In Admin view page, Editor UI Configuration - Default Settings for New Users - Default Language, set up the new user's default language is not effective. I changed this web.py, add a line of code in 1248 lines in register function. Creating the new user need to take the default language.
2022-11-08 01:32:38 +08:00
Ghighi Eftimie 9409b9db9c fixes for 946, 2284 caliblur 2022-10-21 21:53:15 +02:00
Ghighi Eftimie a992aafc13 fixes #2515 2022-10-21 15:08:33 +02:00
Ghighi Eftimie b663f1ce83 added font size fader in reader's settings popup #487 2022-10-16 16:22:08 +02:00
Olivier adfbd447ed mp3 player was missing the csrf_token 2022-10-11 19:50:23 +09:00
xlivevil 73567db4fb Update douban metadata provider
Change search API
Change to Return an empty list when an error occurs
Change the way to get tags
Fix series and publisher perse error
Rename a variable with the same name as the built-in
2022-10-10 01:49:42 +08:00
Ozzieisaacs 3d59a78c9f little refactoring for send emails through gmail account 2022-10-04 20:48:09 +02:00
Ozzieisaacs 8ba23ac3ee Merge remote-tracking branch 'gmail/issue/2471' 2022-10-04 20:46:19 +02:00
Ghighi Eftimie 397cd987cb added try except block for send_gmail_email method 2022-10-04 21:28:09 +03:00
Ozzie Isaacs 7eef44f73c Make drive letters available in file picker 2022-10-03 15:17:17 +02:00
Ozzie Isaacs e22ecda137 Merge remote-tracking branch 'it/patch-30' 2022-10-02 21:12:56 +02:00
ElQuimm a003cd9758 update message.po (italian)
Hello, I ran the October 2 update, and this error appeared: 

Merge remote-tracking branch 'it/patch-28'
# Conflicts:
# cps/translations/it/LC_MESSAGES/messages.po

Calibre-Web does not start again and the container logfile does not allow me to have more information.
I proceeded to verify the translation file (I did not find anything strange) and I made some updates of the text.
2022-10-02 21:07:24 +02:00
Ozzie Isaacs 44f6655dd2 Catch one additional database error on edit book 2022-10-02 15:21:53 +02:00
Ozzie Isaacs bd52f08a30 Fix for #2547 (None isn't iterable, so in case scholary request fails, empty list has to be returned) 2022-10-02 15:05:07 +02:00
Ozzie Isaacs edc9703716 Merge remote-tracking branch 'vi/add-translation' 2022-10-02 11:45:00 +02:00
Ozzie Isaacs 56d697122c Merge remote-tracking branch 'it/patch-28'
# Conflicts:
#	cps/translations/it/LC_MESSAGES/messages.po
2022-10-02 11:39:20 +02:00
Ozzie Isaacs d39a43e838 Merge remote-tracking branch 'cn/Translation/Simplifield_Chinese' 2022-10-02 11:36:47 +02:00
ElQuimm 9df3a2558d update message.po
updated italian message.po translation
2022-09-29 11:07:31 +02:00
xlivevil 7339c804a3 fix typo 2022-09-29 13:18:21 +08:00
xlivevil 4d61c5535e Update Simplifield Chinese translation 2022-09-28 20:14:58 +08:00
xlivevil 09e1ec3d08 fix typo 2022-09-28 20:12:21 +08:00
Ozzie Isaacs 8421a017f4 Updated Test result 2022-09-26 16:54:15 +02:00
Ozzie Isaacs 27eb514ca4 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	cps/static/js/table.js
2022-09-25 19:57:09 +02:00
Ozzie Isaacs b4d9e400d9 Handle None as identifier value during upload 2022-09-25 19:39:38 +02:00
Ozzie Isaacs 67bc23ee0c Fix for #2537 (Impossible to set Denied Column Value from user table) 2022-09-25 19:39:38 +02:00
Ozzie Isaacs b898b37e29 Fix for #2545 (Max task duration double entry "1hour") 2022-09-25 19:39:38 +02:00
Ozzie Isaacs 10dcf39d50 Merge branch '1' 2022-09-25 19:37:55 +02:00
Ozzie Isaacs e676e1685b Handle None as identifier value during upload 2022-09-25 19:37:38 +02:00
Ozzie Isaacs 59a5ccd05c Bugfixes after testrun 2022-09-25 19:36:40 +02:00
Ozzie Isaacs 04908e22fe backup metadata 5th step 2022-09-23 20:45:30 +02:00
Ozzie Isaacs 0f67e57be4 Merge branch 'master' 2022-09-20 19:12:36 +02:00
Ozzie Isaacs 071d19b8b3 Fix for #2537 (Impossible to set Denied Column Value from user table) 2022-09-20 19:10:45 +02:00
halink0803 1ffa190938 add vietnamese translation 2022-09-20 21:38:47 +07:00
Ozzie Isaacs c10708ed07 Backup metadata 4th step 2022-09-19 22:39:40 +02:00
Ozzie Isaacs b4851e1d70 Fix for #2545 (Max task duration double entry "1hour") 2022-09-19 18:57:55 +02:00
Ozzie Isaacs 26be5ee237 Backup metadata 3rd step 2022-09-19 18:56:31 +02:00
Ozzieisaacs 241aa77d41 backup metadata second step 2022-09-14 17:03:48 +02:00
Ozzieisaacs ca0ee5d391 backup metadata first step 2022-09-10 18:26:52 +02:00
Ozzieisaacs 110d283a50 Enable series type custom column 2022-09-10 17:17:20 +02:00
Giulio De Pasquale f6a9030c33 Removed extra space 2022-09-08 17:26:34 +02:00
Giulio De Pasquale 452093db47 Google Covers: strip curl in thumbnail and request higher resolution image 2022-09-08 17:23:53 +02:00
Ozzieisaacs 9fa56a2323 Merge remote-tracking branch 'kobolanguage/language' 2022-09-06 18:09:07 +02:00
Ozzieisaacs 3a133901e4 Fix: ignore special files originating from Apple devices 2022-09-06 18:06:59 +02:00
Ozzieisaacs 7750ebde0f Update pdf Reader 2022-09-05 19:42:02 +02:00
Ozzieisaacs 2472e03a69 Bugfix ratelimiter kobo 2022-09-05 18:45:24 +02:00
Ozzieisaacs 6598c4d259 Add rate limit for opds 2022-09-04 19:47:04 +02:00
Ozzie Isaacs a9b20ca136 Fix for big database not showing tags 2022-08-29 19:08:04 +02:00
Ozzie Isaacs bf0375d51d Bugfix change emails 2022-08-28 15:59:25 +02:00
Ozzie Isaacs 89d226e36b Allow deletion of kindle email address and force e-mail address to be valid 2022-08-28 15:54:43 +02:00
Ozzie Isaacs ec8844c7d4 Make pyPDF2 again to the favorite pdf metadata extractor 2022-08-27 15:44:21 +02:00
Ozzie Isaacs e5c8a7ce50 Change landing page for issues, to reduce number of empty issues 2022-08-27 10:20:53 +02:00
Ozzie Isaacs dc3cafd23d Debug message improved (fix for #2516) 2022-08-27 10:13:38 +02:00
Ozzie Isaacs 9de474e665 Add galician language to available translations (#2510) 2022-08-27 10:03:01 +02:00
Martin Brodbeck cd143b7ef4 Use part1 instead of part3 language codes 2022-08-12 15:18:50 +02:00
Martin Brodbeck 8a5112502d Considers the language of the ebook instead of always specifying "English". 2022-08-12 11:51:26 +02:00
viljasenville 46e5305f23 Comic reader: ignore special files originating from Apple devices 2022-07-27 11:17:20 +03:00
Ozzie Isaacs ae3e3559b8 Rate limit prepared for feedback on login route 2022-07-18 10:59:54 +02:00
Ozzie Isaacs 7344ef353c Rate limited login 2022-07-02 19:46:58 +02:00
Ozzie Isaacs 3bde8a5d95 Encrypt passwords 2022-07-02 17:45:24 +02:00
Ozzie Isaacs b1c70d5b4a Update Teststatus 2022-06-18 18:44:02 +02:00
Ozzieisaacs c5fc30a1be Bugfix error message missing custom read status column
Bugfix password validation
2022-06-17 14:49:42 +02:00
Ozzie Isaacs 29fd4ae4a2 Bugfixes create users
Update Teststatus
2022-06-17 10:14:33 +02:00
Ozzieisaacs 4ef8c35fb7 Bugfies password validation from testrun 2022-06-16 14:16:00 +02:00
Ozzieisaacs 04326af2da password validation working 2022-06-16 11:15:17 +02:00
Ozzieisaacs d6a31e5db8 config verify password working 2022-06-16 10:44:42 +02:00
Ozzie Isaacs 73d48e4ac1 Frontend for password strength 2022-06-16 08:33:39 +02:00
Ozzie Isaacs b206b7a5d8 Merge branch 'master' into Develop 2022-06-14 18:45:50 +02:00
Thore Schillmann 2816a75c3e changed datetime format of published tag 2022-06-09 20:35:44 +00:00
GarcaMan bf12542df5 Updated Rotate Left/Right shortcut funtions to update inmediatly
Minor fixes
2022-06-05 19:56:34 +00:00
Thore Schillmann 0f3f918153 multiple authors and publication date in opds feed 2022-05-22 17:40:21 +00:00
Evan Peterson 7ae9f89bbf Merge branch 'Develop' into kobo-sync-detect-fixed-layout 2022-05-14 10:02:31 -04:00
Evan Peterson 4eaa9413f9 Kobo metadata return correct layout format for fixed layout 2022-01-10 15:15:19 -05:00
GarcaMan e2eab808c0 Merge branch 'long_strip_cbrreader' of github.com:GarckaMan/calibre-web into long_strip_cbrreader 2021-12-02 18:20:27 +00:00
GarcaMan 3ac08a8c0d After toggling fullscreen, focus on main container 2021-12-02 18:20:14 +00:00
Denis Rodríguez 3f56f0dca7 Removed parameter that was wrongly added 2021-11-25 01:30:20 -03:00
GarcaMan 7fc04b353b Selecting Position will not scroll the current image up 2021-11-24 20:22:10 +00:00
GarcaMan a8689ae26b first commit 2021-11-24 19:41:07 +00:00
354 changed files with 121608 additions and 73287 deletions
+1
View File
@@ -1,4 +1,5 @@
constants.py ident export-subst
/test export-ignore
/library export-ignore
cps/static/css/libs/* linguist-vendored
cps/static/js/libs/* linguist-vendored
+2
View File
@@ -28,8 +28,10 @@ cps/cache
.idea/
*.bak
*.log.*
.key
settings.yaml
gdrive_credentials
client_secrets.json
gmail.json
/.key
+2 -2
View File
@@ -26,9 +26,9 @@ The Calibre-Web documentation is hosted in the Github [Wiki](https://github.com/
Do not open up a GitHub issue if the bug is a **security vulnerability** in Calibre-Web. Instead, please write an email to "ozzie.fernandez.isaacs@googlemail.com".
Ensure the ***bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
Ensure the **bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=bug_report.md&title=). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you providing the right information. Deleting the form and just pasting the stack trace doesn't speed up fixing the problem. If your issue could be resolved, consider closing the issue.
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.
### **Feature Request**
+12 -2
View File
@@ -19,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup
- User management with fine-grained per-user permissions
- Admin interface
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, korean, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
- User Interface in brazilian, czech, dutch, english, finnish, french, galician, german, greek, hungarian, indonesian, italian, japanese, khmer, korean, norwegian, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian, vietnamese
- OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series, book format and language
- Create a custom book collection (shelves)
@@ -46,13 +46,20 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
3. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-and-Windows) for details
4. Calibre-Web can be started afterwards by typing `cps`
Issues with Raspberry Pi - Raspberry Pi OS:
Depending on your version of pip it's possible that the installation fails with `Failed to build cryptography
ERROR: Could not build wheels for cryptography, which is required to install pyproject.toml-based projects`.
In this case please try to update pip with `./venv/bin/python3 -m pip install --upgrade pip` first, and then try installing Calibre-Web again.
If this isn't working please also install cargo via `sudo apt install cargo`, and try installing Calibre-Web again.
In the Wiki there are also examples for: a [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation), [installation on Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:Install-Calibre-Web-in-Linux-Mint-19-or-20), [installation on a Cloud Provider](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider).
## Quick start
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog \
Login with default admin login \
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button \
If you don't have a Calibre database already, this [database](https://github.com/janeczku/calibre-web/blob/master/library/metadata.db) can be used. **IMPORTATNT** Please move the database out of the calibre-web folder structure, as it will be overwritten during update. \
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button. \
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration) \
Afterwards you can configure your Calibre-Web instance ([Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) on admin page)
@@ -65,12 +72,15 @@ Afterwards you can configure your Calibre-Web instance ([Basic Configuration](ht
python 3.5+
[Download](https://imagemagick.org/script/download.php) Imagemagick to extract covers from epubs. On Windows the additional installation of [ghostscript](https://ghostscript.com/releases/gsdnld.html) might be necessary to extract covers from pdf files. On Linux Imagemagick and Ghostscript can often be installed using the system package manager.
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-ereader feature, or during editing of ebooks metadata:
[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page.
[Download](https://github.com/pgaskin/kepubify/releases/latest) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `/opt/kepubify` Windows: `C:\Program Files\kepubify`.
## Docker Images
A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team):
+2 -1
View File
@@ -1,3 +1,4 @@
[python: **.py]
# has to be executed with jinja2 >=2.9 to have autoescape enabled automatically
[jinja2: **/templates/**.*ml]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
+1 -1
View File
@@ -21,7 +21,7 @@ import os
import sys
# Add local path to sys.path so we can import cps
# Add local path to sys.path, so we can import cps
path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, path)
+20 -3
View File
@@ -21,9 +21,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask_login import LoginManager
from flask import session
from flask_login import LoginManager, confirm_login
from flask import session, current_app
from flask_login.utils import decode_cookie
from flask_login.signals import user_loaded_from_cookie
class MyLoginManager(LoginManager):
def _session_protection_failed(self):
@@ -33,3 +34,19 @@ class MyLoginManager(LoginManager):
and _session.get('csrf_token', None))) and ident != _session.get('_id', None):
return super(). _session_protection_failed()
return False
def _load_user_from_remember_cookie(self, cookie):
user_id = decode_cookie(cookie)
if user_id is not None:
session["_user_id"] = user_id
session["_fresh"] = False
user = None
if self._user_callback:
user = self._user_callback(user_id)
if user is not None:
app = current_app._get_current_object()
user_loaded_from_cookie.send(app, user=user)
# if session was restored from remember me cookie make login valid
confirm_login()
return user
return None
+53 -23
View File
@@ -36,11 +36,16 @@ from .reverseproxy import ReverseProxied
from .server import WebServer
from .dep_check import dependency_check
from .updater import Updater
from .babel import babel
from .babel import babel, get_locale
from . import config_sql
from . import cache_buster
from . import ub, db
try:
from flask_limiter import Limiter
limiter_present = True
except ImportError:
limiter_present = False
try:
from flask_wtf.csrf import CSRFProtect
wtf_present = True
@@ -81,7 +86,7 @@ app.config.update(
lm = MyLoginManager()
config = config_sql._ConfigSQL()
config = config_sql.ConfigSQL()
cli_param = CliParameter()
@@ -96,33 +101,36 @@ web_server = WebServer()
updater_thread = Updater()
if limiter_present:
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True)
else:
limiter = None
def create_app():
lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous
lm.session_protection = 'strong'
if csrf:
csrf.init_app(app)
cli_param.init()
ub.init_db(cli_param.settings_path, cli_param.user_credentials)
ub.init_db(cli_param.settings_path)
# pylint: disable=no-member
config_sql.load_configuration(config, ub.session, cli_param)
encrypt_key, error = config_sql.get_encryption_key(os.path.dirname(cli_param.settings_path))
db.CalibreDB.update_config(config)
db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path)
calibre_db.init_db()
config_sql.load_configuration(ub.session, encrypt_key)
config.init_config(ub.session, encrypt_key, cli_param)
updater_thread.init_updater(config, web_server)
# Perform dry run of updater and exit afterwards
if cli_param.dry_run:
updater_thread.dry_run()
sys.exit(0)
updater_thread.start()
if error:
log.error(error)
ub.password_change(cli_param.user_credentials)
if not limiter:
log.info('*** "flask-limiter" is needed for calibre-web to run. '
'Please install it using pip: "pip install flask-limiter" ***')
print('*** "flask-limiter" is needed for calibre-web to run. '
'Please install it using pip: "pip install flask-limiter" ***')
web_server.stop(True)
sys.exit(8)
if sys.version_info < (3, 0):
log.info(
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
@@ -139,8 +147,24 @@ def create_app():
'Please install it using pip: "pip install flask-WTF" ***')
web_server.stop(True)
sys.exit(7)
lm.login_view = 'web.login'
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()
updater_thread.init_updater(config, web_server)
# Perform dry run of updater and exit afterwards
if cli_param.dry_run:
updater_thread.dry_run()
sys.exit(0)
updater_thread.start()
for res in dependency_check() + dependency_check(True):
log.info('*** "{}" version does not fit the requirements. '
log.info('*** "{}" version does not meet the requirements. '
'Should: {}, Found: {}, please consider installing required version ***'
.format(res['name'],
res['target'],
@@ -150,14 +174,16 @@ def create_app():
if os.environ.get('FLASK_DEBUG'):
cache_buster.init_cache_busting(app)
log.info('Starting Calibre Web...')
Principal(app)
lm.init_app(app)
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
web_server.init_app(app, config)
babel.init_app(app)
if hasattr(babel, "localeselector"):
babel.init_app(app)
babel.localeselector(get_locale)
else:
babel.init_app(app, locale_selector=get_locale)
from . import services
@@ -165,9 +191,13 @@ def create_app():
services.ldap.init_app(app, config)
if services.goodreads_support:
services.goodreads_support.connect(config.config_goodreads_api_key,
config.config_goodreads_api_secret,
config.config_goodreads_api_secret_e,
config.config_use_goodreads)
config.store_calibre_uuid(calibre_db, db.Library_Id)
# Configure rate limiter
app.config.update(RATELIMIT_ENABLED=config.config_ratelimiter)
limiter.init_app(app)
# Register scheduled tasks
from .schedule import register_scheduled_tasks, register_startup_tasks
register_scheduled_tasks(config.schedule_reconnect)
+1 -1
View File
@@ -81,4 +81,4 @@ def stats():
categories = calibre_db.session.query(db.Tags).count()
series = calibre_db.session.query(db.Series).count()
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
categorycounter=categories, seriecounter=series, title=_(u"Statistics"), page="stat")
categorycounter=categories, seriecounter=series, title=_("Statistics"), page="stat")
Executable → Regular
+197 -151
View File
@@ -22,19 +22,20 @@
import os
import re
import base64
import json
import operator
import time
import sys
import string
from datetime import datetime, timedelta
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_login import login_required, current_user, logout_user, confirm_login
from flask_login import login_required, current_user, logout_user
from flask_babel import gettext as _
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
from flask import session as flask_session
from sqlalchemy import and_
from sqlalchemy.orm.attributes import flag_modified
@@ -52,27 +53,28 @@ from .services.worker import WorkerThread
from .babel import get_available_translations, get_available_locale, get_user_locale_language
from . import debug_info
log = logger.create()
feature_support = {
'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo),
'updater': constants.UPDATER_AVAILABLE,
'gmail': bool(services.gmail),
'scheduler': schedule.use_APScheduler,
'gdrive': gdrive_support
}
'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo),
'updater': constants.UPDATER_AVAILABLE,
'gmail': bool(services.gmail),
'scheduler': schedule.use_APScheduler,
'gdrive': gdrive_support
}
try:
import rarfile # pylint: disable=unused-import
feature_support['rar'] = True
except (ImportError, SyntaxError):
feature_support['rar'] = False
try:
from .oauth_bb import oauth_check, oauthblueprints
feature_support['oauth'] = True
except ImportError as err:
log.debug('Cannot import Flask-Dance, login with Oauth will not work: %s', err)
@@ -80,7 +82,6 @@ except ImportError as err:
oauthblueprints = []
oauth_check = {}
admi = Blueprint('admin', __name__)
@@ -100,25 +101,21 @@ def admin_required(f):
@admi.before_app_request
def before_request():
# make remember me function work
if current_user.is_authenticated:
confirm_login()
if not ub.check_user_session(current_user.id, flask_session.get('_id')) and 'opds' not in request.path:
logout_user()
g.constants = constants
g.user = current_user
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','')
g.allow_registration = config.config_public_reg
g.allow_anonymous = config.config_anonbrowse
g.allow_upload = config.config_uploading
g.current_theme = config.config_theme
g.config_authors_max = config.config_authors_max
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
if '/static/' not in request.path and not config.db_configured and \
request.endpoint not in ('admin.ajax_db_config',
'admin.simulatedbchange',
'admin.db_configuration',
'web.login',
'web.login_post',
'web.logout',
'admin.load_dialogtexts',
'admin.ajax_pathchooser'):
@@ -136,28 +133,39 @@ def admin_forbidden():
@admin_required
def shutdown():
task = request.get_json().get('parameter', -1)
showtext = {}
show_text = {}
if task in (0, 1): # valid commandos received
# close all database connections
calibre_db.dispose()
ub.dispose()
if task == 0:
showtext['text'] = _(u'Server restarted, please reload page')
show_text['text'] = _('Server restarted, please reload page.')
else:
showtext['text'] = _(u'Performing shutdown of server, please close window')
show_text['text'] = _('Performing Server shutdown, please close window.')
# stop gevent/tornado server
web_server.stop(task == 0)
return json.dumps(showtext)
return json.dumps(show_text)
if task == 2:
log.warning("reconnecting to calibre database")
calibre_db.reconnect_db(config, ub.app_DB_path)
showtext['text'] = _(u'Reconnect successful')
return json.dumps(showtext)
show_text['text'] = _('Success! Database Reconnected')
return json.dumps(show_text)
showtext['text'] = _(u'Unknown command')
return json.dumps(showtext), 400
show_text['text'] = _('Unknown command')
return json.dumps(show_text), 400
@admi.route("/metadata_backup", methods=["POST"])
@login_required
@admin_required
def queue_metadata_backup():
show_text = {}
log.warning("Queuing all books for metadata backup")
helper.set_all_metadata_dirty()
show_text['text'] = _('Success! Books queued for Metadata Backup, please check Tasks for result')
return json.dumps(show_text)
# method is available without login and not protected by CSRF to make it easy reachable, is per default switched off
@@ -189,14 +197,14 @@ def update_thumbnails():
def admin():
version = updater_thread.get_current_version_info()
if version is False:
commit = _(u'Unknown')
commit = _('Unknown')
else:
if 'datetime' in version:
commit = version['datetime']
tz = timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
form_date = datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S")
if len(commit) > 19: # check if string has timezone
if len(commit) > 19: # check if string has timezone
if commit[19] == '+':
form_date -= timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
elif commit[19] == '-':
@@ -206,15 +214,15 @@ def admin():
commit = version['version']
all_user = ub.session.query(ub.User).all()
email_settings = config.get_mail_settings()
# email_settings = mail_config.get_mail_settings()
schedule_time = format_time(datetime_time(hour=config.schedule_start_time), format="short")
t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60)
schedule_duration = format_timedelta(t, threshold=.99)
return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit,
return render_title_template("admin.html", allUser=all_user, config=config, commit=commit,
feature_support=feature_support, schedule_time=schedule_time,
schedule_duration=schedule_duration,
title=_(u"Admin page"), page="admin")
title=_("Admin page"), page="admin")
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
@@ -234,7 +242,7 @@ def configuration():
config=config,
provider=oauthblueprints,
feature_support=feature_support,
title=_(u"Basic Configuration"), page="config")
title=_("Basic Configuration"), page="config")
@admi.route("/admin/ajaxconfig", methods=["POST"])
@@ -262,9 +270,9 @@ def calibreweb_alive():
@login_required
@admin_required
def view_configuration():
read_column = calibre_db.session.query(db.CustomColumns)\
read_column = calibre_db.session.query(db.CustomColumns) \
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all()
restrict_columns = calibre_db.session.query(db.CustomColumns)\
restrict_columns = calibre_db.session.query(db.CustomColumns) \
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all()
languages = calibre_db.speaking_language()
translations = get_available_locale()
@@ -272,7 +280,7 @@ def view_configuration():
restrictColumns=restrict_columns,
languages=languages,
translations=translations,
title=_(u"UI Configuration"), page="uiconfig")
title=_("UI Configuration"), page="uiconfig")
@admi.route("/admin/usertable")
@@ -283,11 +291,11 @@ def edit_user_table():
languages = calibre_db.speaking_language()
translations = get_available_locale()
all_user = ub.session.query(ub.User)
tags = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
.join(db.Books)\
tags = calibre_db.session.query(db.Tags) \
.join(db.books_tags_link) \
.join(db.Books) \
.filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag'))\
.group_by(text('books_tags_link.tag')) \
.order_by(db.Tags.name).all()
if config.config_restricted_column:
custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all()
@@ -306,7 +314,7 @@ def edit_user_table():
all_roles=constants.ALL_ROLES,
kobo_support=kobo_support,
sidebar_settings=constants.sidebar_settings,
title=_(u"Edit Users"),
title=_("Edit Users"),
page="usertable")
@@ -464,20 +472,20 @@ def edit_list_user(param):
elif param.endswith('role'):
value = int(vals['field_index'])
if user.name == "Guest" and value in \
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
raise Exception(_("Guest can't have this role"))
# check for valid value, last on checks for power of 2 value
if value > 0 and value <= constants.ROLE_VIEWER and (value & value-1 == 0 or value == 1):
if value > 0 and value <= constants.ROLE_VIEWER and (value & value - 1 == 0 or value == 1):
if vals['value'] == 'true':
user.role |= value
elif vals['value'] == 'false':
if value == constants.ROLE_ADMIN:
if not ub.session.query(ub.User).\
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != user.id).count():
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",
'message': _(u"No admin user remaining, can't remove admin role",
'message': _("No admin user remaining, can't remove admin role",
nick=user.name)}]), mimetype='application/json')
user.role &= ~value
else:
@@ -489,7 +497,7 @@ def edit_list_user(param):
if user.name == "Guest" and value == constants.SIDEBAR_READ_AND_UNREAD:
raise Exception(_("Guest can't have this view"))
# check for valid value, last on checks for power of 2 value
if value > 0 and value <= constants.SIDEBAR_LIST and (value & value-1 == 0 or value == 1):
if value > 0 and value <= constants.SIDEBAR_LIST and (value & value - 1 == 0 or value == 1):
if vals['value'] == 'true':
user.sidebar_view |= value
elif vals['value'] == 'false':
@@ -554,13 +562,13 @@ def update_view_configuration():
calibre_db.update_title_sort(config)
if not check_valid_read_column(to_save.get("config_read_column", "0")):
flash(_(u"Invalid Read Column"), category="error")
flash(_("Invalid Read Column"), category="error")
log.debug("Invalid Read column")
return view_configuration()
_config_int(to_save, "config_read_column")
if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")):
flash(_(u"Invalid Restricted Column"), category="error")
flash(_("Invalid Restricted Column"), category="error")
log.debug("Invalid Restricted Column")
return view_configuration()
_config_int(to_save, "config_restricted_column")
@@ -580,7 +588,7 @@ def update_view_configuration():
config.config_default_show |= constants.DETAIL_RANDOM
config.save()
flash(_(u"Calibre-Web configuration updated"), category="success")
flash(_("Calibre-Web configuration updated"), category="success")
log.debug("Calibre-Web configuration updated")
before_request()
@@ -642,7 +650,7 @@ def edit_domain(allow):
@admin_required
def add_domain(allow):
domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower()
check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name)\
check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name) \
.filter(ub.Registration.allow == allow).first()
if not check:
new_domain = ub.Registration(domain=domain_name, allow=allow)
@@ -860,16 +868,16 @@ def delete_restriction(res_type, user_id):
@login_required
@admin_required
def list_restriction(res_type, user_id):
if res_type == 0: # Tags as template
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)}
if res_type == 0: # Tags as template
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
for i, x in enumerate(config.list_denied_tags()) if x != '']
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)}
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
for i, x in enumerate(config.list_allowed_tags()) if x != '']
json_dumps = restrict + allow
elif res_type == 1: # CustomC as template
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)}
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
for i, x in enumerate(config.list_denied_column_values()) if x != '']
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)}
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
for i, x in enumerate(config.list_allowed_column_values()) if x != '']
json_dumps = restrict + allow
elif res_type == 2: # Tags per user
@@ -877,9 +885,9 @@ def list_restriction(res_type, user_id):
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
else:
usr = current_user
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)}
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
for i, x in enumerate(usr.list_denied_tags()) if x != '']
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)}
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
for i, x in enumerate(usr.list_allowed_tags()) if x != '']
json_dumps = restrict + allow
elif res_type == 3: # CustomC per user
@@ -887,9 +895,9 @@ def list_restriction(res_type, user_id):
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
else:
usr = current_user
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)}
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
for i, x in enumerate(usr.list_denied_column_values()) if x != '']
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)}
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
for i, x in enumerate(usr.list_allowed_column_values()) if x != '']
json_dumps = restrict + allow
else:
@@ -919,7 +927,7 @@ def ajax_pathchooser():
def check_valid_read_column(column):
if column != "0":
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all():
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all():
return False
return True
@@ -927,7 +935,7 @@ def check_valid_read_column(column):
def check_valid_restricted_column(column):
if column != "0":
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all():
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all():
return False
return True
@@ -955,7 +963,7 @@ def prepare_tags(user, action, tags_name, id_list):
raise Exception(_("Tag not found"))
new_tags_list = [x.name for x in tags]
else:
tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column])\
tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column]) \
.filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all()
new_tags_list = [x.value for x in tags]
saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else []
@@ -968,6 +976,19 @@ def prepare_tags(user, action, tags_name, id_list):
return ",".join(saved_tags_list)
def get_drives(current):
drive_letters = []
for d in string.ascii_uppercase:
if os.path.exists('{}:'.format(d)) and current[0].lower() != d.lower():
drive = "{}:\\".format(d)
data = {"name": drive, "fullpath": drive}
data["sort"] = "_" + data["fullpath"].lower()
data["type"] = "dir"
data["size"] = ""
drive_letters.append(data)
return drive_letters
def pathchooser():
browse_for = "folder"
folder_only = request.args.get('folder', False) == "true"
@@ -975,40 +996,41 @@ def pathchooser():
path = os.path.normpath(request.args.get('path', ""))
if os.path.isfile(path):
oldfile = path
old_file = path
path = os.path.dirname(path)
else:
oldfile = ""
old_file = ""
absolute = False
if os.path.isdir(path):
# if os.path.isabs(path):
cwd = os.path.realpath(path)
absolute = True
# else:
# cwd = os.path.relpath(path)
else:
cwd = os.getcwd()
cwd = os.path.normpath(os.path.realpath(cwd))
parentdir = os.path.dirname(cwd)
parent_dir = os.path.dirname(cwd)
if not absolute:
if os.path.realpath(cwd) == os.path.realpath("/"):
cwd = os.path.relpath(cwd)
else:
cwd = os.path.relpath(cwd) + os.path.sep
parentdir = os.path.relpath(parentdir) + os.path.sep
parent_dir = os.path.relpath(parent_dir) + os.path.sep
if os.path.realpath(cwd) == os.path.realpath("/"):
parentdir = ""
files = []
if os.path.realpath(cwd) == os.path.realpath("/") \
or (sys.platform == "win32" and os.path.realpath(cwd)[1:] == os.path.realpath("/")[1:]):
# we are in root
parent_dir = ""
if sys.platform == "win32":
files = get_drives(cwd)
try:
folders = os.listdir(cwd)
except Exception:
folders = []
files = []
for f in folders:
try:
data = {"name": f, "fullpath": os.path.join(cwd, f)}
@@ -1041,9 +1063,9 @@ def pathchooser():
context = {
"cwd": cwd,
"files": files,
"parentdir": parentdir,
"parentdir": parent_dir,
"type": browse_for,
"oldfile": oldfile,
"oldfile": old_file,
"absolute": absolute,
}
return json.dumps(context)
@@ -1062,7 +1084,7 @@ def _config_checkbox_int(to_save, x):
def _config_string(to_save, x):
return config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
return config.set_from_dictionary(to_save, x, lambda y: y.strip().strip(u'\u200B\u200C\u200D\ufeff') if y else y)
def _configuration_gdrive_helper(to_save):
@@ -1081,10 +1103,10 @@ def _configuration_gdrive_helper(to_save):
if not gdrive_secrets:
return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
gdriveutils.update_settings(
gdrive_secrets['client_id'],
gdrive_secrets['client_secret'],
gdrive_secrets['redirect_uris'][0]
)
gdrive_secrets['client_id'],
gdrive_secrets['client_secret'],
gdrive_secrets['redirect_uris'][0]
)
# always show Google Drive settings, but in case of error deny support
new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save)
@@ -1101,12 +1123,12 @@ def _configuration_oauth_helper(to_save):
reboot_required = False
for element in oauthblueprints:
if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
reboot_required = True
element['oauth_client_id'] = to_save["config_" + str(element['id']) + "_oauth_client_id"]
element['oauth_client_secret'] = to_save["config_" + str(element['id']) + "_oauth_client_secret"]
if to_save["config_" + str(element['id']) + "_oauth_client_id"] \
and to_save["config_" + str(element['id']) + "_oauth_client_secret"]:
and to_save["config_" + str(element['id']) + "_oauth_client_secret"]:
active_oauths += 1
element["active"] = 1
else:
@@ -1136,7 +1158,6 @@ def _configuration_logfile_helper(to_save):
def _configuration_ldap_helper(to_save):
reboot_required = False
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
reboot_required |= _config_int(to_save, "config_ldap_port")
reboot_required |= _config_int(to_save, "config_ldap_authentication")
reboot_required |= _config_string(to_save, "config_ldap_dn")
@@ -1151,21 +1172,26 @@ def _configuration_ldap_helper(to_save):
reboot_required |= _config_string(to_save, "config_ldap_cert_path")
reboot_required |= _config_string(to_save, "config_ldap_key_path")
_config_string(to_save, "config_ldap_group_name")
if to_save.get("config_ldap_serv_password", "") != "":
address = urlparse(to_save.get("config_ldap_provider_url", ""))
to_save["config_ldap_provider_url"] = (address.hostname or address.path).strip("/")
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
if to_save.get("config_ldap_serv_password_e", "") != "":
reboot_required |= 1
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8')
config.set_from_dictionary(to_save, "config_ldap_serv_password_e")
config.save()
if not config.config_ldap_provider_url \
or not config.config_ldap_port \
or not config.config_ldap_dn \
or not config.config_ldap_user_object:
or not config.config_ldap_user_object:
return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, '
'Port, DN and User Object Identifier'))
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password):
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password_e):
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password'))
else:
if not config.config_ldap_serv_username:
@@ -1229,16 +1255,16 @@ def new_user():
content.default_language = config.config_default_language
return render_title_template("user_edit.html", new_user=1, content=content,
config=config, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser",
languages=languages, title=_("Add New User"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check)
@admi.route("/admin/mailsettings")
@admi.route("/admin/mailsettings", methods=["GET"])
@login_required
@admin_required
def edit_mailsettings():
content = config.get_mail_settings()
return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"),
return render_title_template("email_edit.html", content=content, title=_("Edit Email Server Settings"),
page="mailset", feature_support=feature_support)
@@ -1257,7 +1283,7 @@ def update_mailsettings():
elif to_save.get("gmail"):
try:
config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token)
flash(_(u"Gmail Account Verification Successful"), category="success")
flash(_("Success! Gmail Account Verified."), category="success")
except Exception as ex:
flash(str(ex), category="error")
log.error(ex)
@@ -1266,8 +1292,8 @@ def update_mailsettings():
else:
_config_int(to_save, "mail_port")
_config_int(to_save, "mail_use_ssl")
_config_string(to_save, "mail_password")
_config_int(to_save, "mail_size", lambda y: int(y)*1024*1024)
_config_string(to_save, "mail_password_e")
_config_int(to_save, "mail_size", lambda y: int(y) * 1024 * 1024)
config.mail_server = to_save.get('mail_server', "").strip()
config.mail_from = to_save.get('mail_from', "").strip()
config.mail_login = to_save.get('mail_login', "").strip()
@@ -1276,24 +1302,24 @@ def update_mailsettings():
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return edit_mailsettings()
except Exception as e:
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return edit_mailsettings()
if to_save.get("test"):
if current_user.email:
result = send_test_mail(current_user.email, current_user.name)
if result is None:
flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result",
flash(_("Test e-mail queued for sending to %(email)s, please check Tasks for result",
email=current_user.email), category="info")
else:
flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error")
flash(_("There was an error sending the Test e-mail: %(res)s", res=result), category="error")
else:
flash(_(u"Please configure your e-mail address first..."), category="error")
flash(_("Please configure your e-mail address first..."), category="error")
else:
flash(_(u"E-mail server settings updated"), category="success")
flash(_("Email Server Settings updated"), category="success")
return edit_mailsettings()
@@ -1307,16 +1333,16 @@ def edit_scheduledtasks():
duration_field = list()
for n in range(24):
time_field.append((n, format_time(datetime_time(hour=n), format="short",)))
time_field.append((n, format_time(datetime_time(hour=n), format="short", )))
for n in range(5, 65, 5):
t = timedelta(hours=n // 60, minutes=n % 60)
duration_field.append((n, format_timedelta(t, threshold=.9)))
duration_field.append((n, format_timedelta(t, threshold=.97)))
return render_title_template("schedule_edit.html",
config=content,
starttime=time_field,
duration=duration_field,
title=_(u"Edit Scheduled Tasks Settings"))
title=_("Edit Scheduled Tasks Settings"))
@admi.route("/admin/scheduledtasks", methods=["POST"])
@@ -1326,23 +1352,24 @@ def update_scheduledtasks():
error = False
to_save = request.form.to_dict()
if 0 <= int(to_save.get("schedule_start_time")) <= 23:
_config_int(to_save, "schedule_start_time")
_config_int( to_save, "schedule_start_time")
else:
flash(_(u"Invalid start time for task specified"), category="error")
flash(_("Invalid start time for task specified"), category="error")
error = True
if 0 < int(to_save.get("schedule_duration")) <= 60:
_config_int(to_save, "schedule_duration")
else:
flash(_(u"Invalid duration for task specified"), category="error")
flash(_("Invalid duration for task specified"), category="error")
error = True
_config_checkbox(to_save, "schedule_generate_book_covers")
_config_checkbox(to_save, "schedule_generate_series_covers")
_config_checkbox(to_save, "schedule_metadata_backup")
_config_checkbox(to_save, "schedule_reconnect")
if not error:
try:
config.save()
flash(_(u"Scheduled tasks settings updated"), category="success")
flash(_("Scheduled tasks settings updated"), category="success")
# Cancel any running tasks
schedule.end_scheduled_tasks()
@@ -1352,7 +1379,7 @@ def update_scheduledtasks():
except IntegrityError:
ub.session.rollback()
log.error("An unknown error occurred while saving scheduled tasks settings")
flash(_(u"An unknown error occurred. Please try again later."), category="error")
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
except OperationalError:
ub.session.rollback()
log.error("Settings DB is not Writeable")
@@ -1367,7 +1394,7 @@ def update_scheduledtasks():
def edit_user(user_id):
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
if not content or (not config.config_anonbrowse and content.name == "Guest"):
flash(_(u"User not found"), category="error")
flash(_("User not found"), category="error")
return redirect(url_for('admin.admin'))
languages = calibre_db.speaking_language(return_all_languages=True)
translations = get_available_locale()
@@ -1386,7 +1413,7 @@ def edit_user(user_id):
registered_oauth=oauth_check,
mail_configured=config.get_mail_server_configured(),
kobo_support=kobo_support,
title=_(u"Edit User %(nick)s", nick=content.name),
title=_("Edit User %(nick)s", nick=content.name),
page="edituser")
@@ -1397,14 +1424,14 @@ def reset_user_password(user_id):
if current_user is not None and current_user.is_authenticated:
ret, message = reset_password(user_id)
if ret == 1:
log.debug(u"Password for user %s reset", message)
flash(_(u"Password for user %(user)s reset", user=message), category="success")
log.debug("Password for user %s reset", message)
flash(_("Success! Password for user %(user)s reset", user=message), category="success")
elif ret == 0:
log.error(u"An unknown error occurred. Please try again later.")
flash(_(u"An unknown error occurred. Please try again later."), category="error")
log.error("An unknown error occurred. Please try again later.")
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
else:
log.error(u"Please configure the SMTP mail settings first...")
flash(_(u"Please configure the SMTP mail settings first..."), category="error")
log.error("Please configure the SMTP mail settings.")
flash(_("Oops! Please configure the SMTP mail settings."), category="error")
return redirect(url_for('admin.admin'))
@@ -1415,7 +1442,7 @@ def view_logfile():
logfiles = {0: logger.get_logfile(config.config_logfile),
1: logger.get_accesslogfile(config.config_access_logfile)}
return render_title_template("logviewer.html",
title=_(u"Logfile viewer"),
title=_("Logfile viewer"),
accesslog_enable=config.config_access_log,
log_enable=bool(config.config_logfile != logger.LOG_TO_STDOUT),
logfiles=logfiles,
@@ -1465,7 +1492,7 @@ def download_debug():
@admin_required
def get_update_status():
if feature_support['updater']:
log.info(u"Update status requested")
log.info("Update status requested")
return updater_thread.get_available_updates(request.method)
else:
return ''
@@ -1558,7 +1585,7 @@ def ldap_import_create_user(user, user_data):
ub.session.add(content)
try:
ub.session.commit()
return 1, None # increase no of users
return 1, None # increase no of users
except Exception as ex:
log.warning("Failed to create LDAP user: %s - %s", user, ex)
ub.session.rollback()
@@ -1660,7 +1687,7 @@ def _db_configuration_update_helper():
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
_db_configuration_result(_(u"Database error: %(error)s.", error=e.orig), gdrive_error)
_db_configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig), gdrive_error)
try:
metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db")
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
@@ -1670,7 +1697,7 @@ def _db_configuration_update_helper():
return _db_configuration_result('{}'.format(ex), gdrive_error)
if db_change or not db_valid or not config.db_configured \
or config.config_calibre_dir != to_save["config_calibre_dir"]:
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:
@@ -1692,7 +1719,7 @@ def _db_configuration_update_helper():
_config_string(to_save, "config_calibre_dir")
calibre_db.update_config(config)
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_(u"DB is not Writeable"), category="warning")
flash(_("DB is not Writeable"), category="warning")
config.save()
return _db_configuration_result(None, gdrive_error)
@@ -1749,10 +1776,10 @@ def _configuration_update_helper():
# Goodreads configuration
_config_checkbox(to_save, "config_use_goodreads")
_config_string(to_save, "config_goodreads_api_key")
_config_string(to_save, "config_goodreads_api_secret")
_config_string(to_save, "config_goodreads_api_secret_e")
if services.goodreads_support:
services.goodreads_support.connect(config.config_goodreads_api_key,
config.config_goodreads_api_secret,
config.config_goodreads_api_secret_e,
config.config_use_goodreads)
_config_int(to_save, "config_updatechannel")
@@ -1765,10 +1792,25 @@ def _configuration_update_helper():
if config.config_login_type == constants.LOGIN_OAUTH:
reboot_required |= _configuration_oauth_helper(to_save)
# logfile configuration
reboot, message = _configuration_logfile_helper(to_save)
if message:
return message
reboot_required |= reboot
# security configuration
_config_checkbox(to_save, "config_password_policy")
_config_checkbox(to_save, "config_password_number")
_config_checkbox(to_save, "config_password_lower")
_config_checkbox(to_save, "config_password_upper")
_config_checkbox(to_save, "config_password_special")
if 0 < int(to_save.get("config_password_min_length", "0")) < 41:
_config_int(to_save, "config_password_min_length")
else:
return _configuration_result(_('Password length has to be between 1 and 40'))
reboot_required |= _config_int(to_save, "config_session")
reboot_required |= _config_checkbox(to_save, "config_ratelimiter")
# Rarfile Content configuration
_config_string(to_save, "config_rarfile_location")
if "config_rarfile_location" in to_save:
@@ -1778,7 +1820,7 @@ def _configuration_update_helper():
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
_configuration_result(_(u"Database error: %(error)s.", error=e.orig))
_configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig))
config.save()
if reboot_required:
@@ -1794,7 +1836,7 @@ def _configuration_result(error_flash=None, reboot=False):
config.load()
resp['result'] = [{'type': "danger", 'message': error_flash}]
else:
resp['result'] = [{'type': "success", 'message': _(u"Calibre-Web configuration updated")}]
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')
@@ -1825,7 +1867,7 @@ def _db_configuration_result(error_flash=None, gdrive_error=None):
gdriveError=gdrive_error,
gdrivefolders=gdrivefolders,
feature_support=feature_support,
title=_(u"Database Configuration"), page="dbconfig")
title=_("Database Configuration"), page="dbconfig")
def _handle_new_user(to_save, content, languages, translations, kobo_support):
@@ -1837,11 +1879,11 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.sidebar_view |= constants.DETAIL_RANDOM
content.role = constants.selected_roles(to_save)
content.password = generate_password_hash(to_save["password"])
try:
if not to_save["name"] or not to_save["email"] or not to_save["password"]:
log.info("Missing entries on new user")
raise Exception(_(u"Please fill out all fields!"))
raise Exception(_("Oops! Please complete all fields."))
content.password = generate_password_hash(helper.valid_password(to_save.get("password", "")))
content.email = check_email(to_save["email"])
# Query username, if not existing, change
content.name = check_username(to_save["name"])
@@ -1849,13 +1891,13 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.kindle_mail = valid_email(to_save["kindle_mail"])
if config.config_public_reg and not check_valid_domain(content.email):
log.info("E-mail: {} for new user is not from valid domain".format(content.email))
raise Exception(_(u"E-mail is not from valid domain"))
raise Exception(_("E-mail is not from valid domain"))
except Exception as ex:
flash(str(ex), category="error")
return render_title_template("user_edit.html", new_user=1, content=content,
config=config,
translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser",
languages=languages, title=_("Add new user"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check)
try:
content.allowed_tags = config.config_allowed_tags
@@ -1866,17 +1908,17 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on"
ub.session.add(content)
ub.session.commit()
flash(_(u"User '%(user)s' created", user=content.name), category="success")
flash(_("User '%(user)s' created", user=content.name), category="success")
log.debug("User {} created".format(content.name))
return redirect(url_for('admin.admin'))
except IntegrityError:
ub.session.rollback()
log.error("Found an existing account for {} or {}".format(content.name, content.email))
flash(_("Found an existing account for this e-mail address or name."), category="error")
flash(_("Oops! An account already exists for this Email. or name."), category="error")
except OperationalError as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
def _delete_user(content):
@@ -1904,10 +1946,10 @@ def _delete_user(content):
log.info("User {} deleted".format(content.name))
return _("User '%(nick)s' deleted", nick=content.name)
else:
log.warning(_("Can't delete Guest User"))
# log.warning(_("Can't delete Guest User"))
raise Exception(_("Can't delete Guest User"))
else:
log.warning("No admin user remaining, can't delete user")
# log.warning("No admin user remaining, can't delete user")
raise Exception(_("No admin user remaining, can't delete user"))
@@ -1925,14 +1967,6 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
log.warning("No admin user remaining, can't remove admin role from {}".format(content.name))
flash(_("No admin user remaining, can't remove admin role"), category="error")
return redirect(url_for('admin.admin'))
if to_save.get("password"):
content.password = generate_password_hash(to_save["password"])
anonymous = content.is_anonymous
content.role = constants.selected_roles(to_save)
if anonymous:
content.role |= constants.ROLE_ANONYMOUS
else:
content.role &= ~constants.ROLE_ANONYMOUS
val = [int(k[5:]) for k in to_save if k.startswith('show_')]
sidebar, __ = get_sidebar_config()
@@ -1960,8 +1994,20 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
if to_save.get("locale"):
content.locale = to_save["locale"]
try:
if to_save.get("email", content.email) != content.email:
content.email = check_email(to_save["email"])
anonymous = content.is_anonymous
content.role = constants.selected_roles(to_save)
if anonymous:
content.role |= constants.ROLE_ANONYMOUS
else:
content.role &= ~constants.ROLE_ANONYMOUS
if to_save.get("password", ""):
content.password = generate_password_hash(helper.valid_password(to_save.get("password", "")))
new_email = valid_email(to_save.get("email", content.email))
if not new_email:
raise Exception(_("Email can't be empty and has to be a valid Email"))
if new_email != content.email:
content.email = check_email(new_email)
# Query username, if not existing, change
if to_save.get("name", content.name) != content.name:
if to_save.get("name") == "Guest":
@@ -1981,19 +2027,19 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
content=content,
config=config,
registered_oauth=oauth_check,
title=_(u"Edit User %(nick)s", nick=content.name),
title=_("Edit User %(nick)s", nick=content.name),
page="edituser")
try:
ub.session_commit()
flash(_(u"User '%(nick)s' updated", nick=content.name), category="success")
flash(_("User '%(nick)s' updated", nick=content.name), category="success")
except IntegrityError as ex:
ub.session.rollback()
log.error("An unknown error occurred while changing user: {}".format(str(ex)))
flash(_(u"An unknown error occurred. Please try again later."), category="error")
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
except OperationalError as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return ""
+6 -7
View File
@@ -1,7 +1,8 @@
from babel import negotiate_locale
from flask_babel import Babel, Locale
from babel.core import UnknownLocaleError
from flask import request, g
from flask import request
from flask_login import current_user
from . import logger
@@ -9,14 +10,12 @@ log = logger.create()
babel = Babel()
@babel.localeselector
def get_locale():
# if a user is logged in, use the locale from the user settings
user = getattr(g, 'user', None)
if user is not None and hasattr(user, "locale"):
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale
if current_user is not None and hasattr(current_user, "locale"):
# if the account is the guest account bypass the config lang settings
if current_user.name != 'Guest':
return current_user.locale
preferred = list()
if request.accept_languages:
+1 -1
View File
@@ -138,7 +138,7 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
file_path=tmp_file_path,
extension=original_file_extension,
title=original_file_name,
author=u'Unknown',
author='Unknown',
cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable),
description="",
tags="",
+100 -24
View File
@@ -23,6 +23,10 @@ import json
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import text
from sqlalchemy import exists
from cryptography.fernet import Fernet
import cryptography.exceptions
from base64 import urlsafe_b64decode
try:
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
@@ -56,7 +60,8 @@ class _Settings(_Base):
mail_port = Column(Integer, default=25)
mail_use_ssl = Column(SmallInteger, default=0)
mail_login = Column(String, default='mail@example.com')
mail_password = Column(String, default='mypassword')
mail_password_e = Column(String)
mail_password = Column(String)
mail_from = Column(String, default='automailer <mail@example.com>')
mail_size = Column(Integer, default=25*1024*1024)
mail_server_type = Column(SmallInteger, default=0)
@@ -69,13 +74,12 @@ class _Settings(_Base):
config_certfile = Column(String)
config_keyfile = Column(String)
config_trustedhosts = Column(String, default='')
config_calibre_web_title = Column(String, default=u'Calibre-Web')
config_calibre_web_title = Column(String, default='Calibre-Web')
config_books_per_page = Column(Integer, default=60)
config_random_books = Column(Integer, default=4)
config_authors_max = Column(Integer, default=0)
config_read_column = Column(Integer, default=0)
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
# config_mature_content_tags = Column(String, default='')
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines|Le|La|Les|L\'|Un|Une)\s+')
config_theme = Column(Integer, default=0)
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
@@ -107,6 +111,7 @@ class _Settings(_Base):
config_use_goodreads = Column(Boolean, default=False)
config_goodreads_api_key = Column(String)
config_goodreads_api_secret_e = Column(String)
config_goodreads_api_secret = Column(String)
config_register_email = Column(Boolean, default=False)
config_login_type = Column(Integer, default=0)
@@ -117,7 +122,8 @@ class _Settings(_Base):
config_ldap_port = Column(SmallInteger, default=389)
config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE)
config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
config_ldap_serv_password = Column(String, default="")
config_ldap_serv_password_e = Column(String)
config_ldap_serv_password = Column(String)
config_ldap_encryption = Column(SmallInteger, default=0)
config_ldap_cacert_path = Column(String, default="")
config_ldap_cert_path = Column(String, default="")
@@ -147,24 +153,35 @@ class _Settings(_Base):
schedule_generate_book_covers = Column(Boolean, default=False)
schedule_generate_series_covers = Column(Boolean, default=False)
schedule_reconnect = Column(Boolean, default=False)
schedule_metadata_backup = Column(Boolean, default=False)
config_password_policy = Column(Boolean, default=True)
config_password_min_length = Column(Integer, default=8)
config_password_number = Column(Boolean, default=True)
config_password_lower = Column(Boolean, default=True)
config_password_upper = Column(Boolean, default=True)
config_password_special = Column(Boolean, default=True)
config_session = Column(Integer, default=1)
config_ratelimiter = Column(Boolean, default=True)
def __repr__(self):
return self.__class__.__name__
# Class holds all application specific settings in calibre-web
class _ConfigSQL(object):
class ConfigSQL(object):
# pylint: disable=no-member
def __init__(self):
pass
self.__dict__["dirty"] = list()
def init_config(self, session, cli):
def init_config(self, session, secret_key, cli):
self._session = session
self._settings = None
self.db_configured = None
self.config_calibre_dir = None
self.load()
self._fernet = Fernet(secret_key)
self.cli = cli
self.load()
change = False
if self.config_converterpath == None: # pylint: disable=access-member-before-definition
@@ -293,10 +310,10 @@ class _ConfigSQL(object):
setattr(self, field, new_value)
return True
def toDict(self):
def to_dict(self):
storage = {}
for k, v in self.__dict__.items():
if k[0] != '_' and not k.endswith("password") and not k.endswith("secret") and not k == "cli":
if k[0] != '_' and not k.endswith("_e") and not k == "cli":
storage[k] = v
return storage
@@ -310,7 +327,13 @@ class _ConfigSQL(object):
column = s.__class__.__dict__.get(k)
if column.default is not None:
v = column.default.arg
setattr(self, k, v)
if k.endswith("_e") and v is not None:
try:
setattr(self, k, self._fernet.decrypt(v).decode())
except cryptography.fernet.InvalidToken:
setattr(self, k, "")
else:
setattr(self, k, v)
have_metadata_db = bool(self.config_calibre_dir)
if have_metadata_db:
@@ -332,16 +355,20 @@ class _ConfigSQL(object):
except OperationalError as e:
log.error('Database error: %s', e)
self._session.rollback()
self.__dict__["dirty"] = list()
def save(self):
"""Apply all configuration values to the underlying storage."""
s = self._read_from_storage() # type: _Settings
for k, v in self.__dict__.items():
for k in self.dirty:
if k[0] == '_':
continue
if hasattr(s, k):
setattr(s, k, v)
if k.endswith("_e"):
setattr(s, k, self._fernet.encrypt(self.__dict__[k].encode()))
else:
setattr(s, k, self.__dict__[k])
log.debug("_ConfigSQL updating storage")
self._session.merge(s)
@@ -357,7 +384,6 @@ class _ConfigSQL(object):
log.error(error)
log.warning("invalidating configuration")
self.db_configured = False
# self.config_calibre_dir = None
self.save()
def store_calibre_uuid(self, calibre_db, Library_table):
@@ -369,8 +395,40 @@ class _ConfigSQL(object):
except AttributeError:
pass
def __setattr__(self, attr_name, attr_value):
super().__setattr__(attr_name, attr_value)
self.__dict__["dirty"].append(attr_name)
def _migrate_table(session, orm_class):
def _encrypt_fields(session, secret_key):
try:
session.query(exists().where(_Settings.mail_password_e)).scalar()
except OperationalError:
with session.bind.connect() as conn:
conn.execute("ALTER TABLE settings ADD column 'mail_password_e' String")
conn.execute("ALTER TABLE settings ADD column 'config_goodreads_api_secret_e' String")
conn.execute("ALTER TABLE settings ADD column 'config_ldap_serv_password_e' String")
session.commit()
crypter = Fernet(secret_key)
settings = session.query(_Settings.mail_password, _Settings.config_goodreads_api_secret,
_Settings.config_ldap_serv_password).first()
if settings.mail_password:
session.query(_Settings).update(
{_Settings.mail_password_e: crypter.encrypt(settings.mail_password.encode())})
if settings.config_goodreads_api_secret:
session.query(_Settings).update(
{_Settings.config_goodreads_api_secret_e:
crypter.encrypt(settings.config_goodreads_api_secret.encode())})
if settings.config_ldap_serv_password:
session.query(_Settings).update(
{_Settings.config_ldap_serv_password_e:
crypter.encrypt(settings.config_ldap_serv_password.encode())})
session.commit()
def _migrate_table(session, orm_class, secret_key=None):
if secret_key:
_encrypt_fields(session, secret_key)
changed = False
for column_name, column in orm_class.__dict__.items():
@@ -446,22 +504,18 @@ def autodetect_kepubify_binary():
return ""
def _migrate_database(session):
def _migrate_database(session, secret_key):
# make sure the table is created, if it does not exist
_Base.metadata.create_all(session.bind)
_migrate_table(session, _Settings)
_migrate_table(session, _Settings, secret_key)
_migrate_table(session, _Flask_Settings)
def load_configuration(conf, session, cli):
_migrate_database(session)
def load_configuration(session, secret_key):
_migrate_database(session, secret_key)
if not session.query(_Settings).count():
session.add(_Settings())
session.commit()
# conf = _ConfigSQL()
conf.init_config(session, cli)
# return conf
def get_flask_session_key(_session):
@@ -471,3 +525,25 @@ def get_flask_session_key(_session):
_session.add(flask_settings)
_session.commit()
return flask_settings.flask_session_key
def get_encryption_key(key_path):
key_file = os.path.join(key_path, ".key")
generate = True
error = ""
if os.path.exists(key_file) and os.path.getsize(key_file) > 32:
with open(key_file, "rb") as f:
key = f.read()
try:
urlsafe_b64decode(key)
generate = False
except ValueError:
pass
if generate:
key = Fernet.generate_key()
try:
with open(key_file, "wb") as f:
f.write(key)
except PermissionError as e:
error = e
return key, error
+1 -1
View File
@@ -163,7 +163,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages, publisher, pubdate, identifiers')
STABLE_VERSION = {'version': '0.6.19'}
STABLE_VERSION = {'version': '0.6.20'}
NIGHTLY_VERSION = dict()
NIGHTLY_VERSION[0] = '$Format:%H$'
+88 -36
View File
@@ -111,66 +111,70 @@ class Identifiers(Base):
def format_type(self):
format_type = self.type.lower()
if format_type == 'amazon':
return u"Amazon"
return "Amazon"
elif format_type.startswith("amazon_"):
return u"Amazon.{0}".format(format_type[7:])
return "Amazon.{0}".format(format_type[7:])
elif format_type == "isbn":
return u"ISBN"
return "ISBN"
elif format_type == "doi":
return u"DOI"
return "DOI"
elif format_type == "douban":
return u"Douban"
return "Douban"
elif format_type == "goodreads":
return u"Goodreads"
return "Goodreads"
elif format_type == "babelio":
return u"Babelio"
return "Babelio"
elif format_type == "google":
return u"Google Books"
return "Google Books"
elif format_type == "kobo":
return u"Kobo"
return "Kobo"
elif format_type == "litres":
return u"ЛитРес"
return "ЛитРес"
elif format_type == "issn":
return u"ISSN"
return "ISSN"
elif format_type == "isfdb":
return u"ISFDB"
return "ISFDB"
if format_type == "lubimyczytac":
return u"Lubimyczytac"
return "Lubimyczytac"
if format_type == "databazeknih":
return "Databáze knih"
else:
return self.type
def __repr__(self):
format_type = self.type.lower()
if format_type == "amazon" or format_type == "asin":
return u"https://amazon.com/dp/{0}".format(self.val)
return "https://amazon.com/dp/{0}".format(self.val)
elif format_type.startswith('amazon_'):
return u"https://amazon.{0}/dp/{1}".format(format_type[7:], self.val)
return "https://amazon.{0}/dp/{1}".format(format_type[7:], self.val)
elif format_type == "isbn":
return u"https://www.worldcat.org/isbn/{0}".format(self.val)
return "https://www.worldcat.org/isbn/{0}".format(self.val)
elif format_type == "doi":
return u"https://dx.doi.org/{0}".format(self.val)
return "https://dx.doi.org/{0}".format(self.val)
elif format_type == "goodreads":
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
return "https://www.goodreads.com/book/show/{0}".format(self.val)
elif format_type == "babelio":
return u"https://www.babelio.com/livres/titre/{0}".format(self.val)
return "https://www.babelio.com/livres/titre/{0}".format(self.val)
elif format_type == "douban":
return u"https://book.douban.com/subject/{0}".format(self.val)
return "https://book.douban.com/subject/{0}".format(self.val)
elif format_type == "google":
return u"https://books.google.com/books?id={0}".format(self.val)
return "https://books.google.com/books?id={0}".format(self.val)
elif format_type == "kobo":
return u"https://www.kobo.com/ebook/{0}".format(self.val)
return "https://www.kobo.com/ebook/{0}".format(self.val)
elif format_type == "lubimyczytac":
return u"https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val)
return "https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val)
elif format_type == "litres":
return u"https://www.litres.ru/{0}".format(self.val)
return "https://www.litres.ru/{0}".format(self.val)
elif format_type == "issn":
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
return "https://portal.issn.org/resource/ISSN/{0}".format(self.val)
elif format_type == "isfdb":
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
return "http://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 self.val.lower().startswith("javascript:"):
return quote(self.val)
else:
return u"{0}".format(self.val)
return "{0}".format(self.val)
class Comments(Base):
@@ -188,7 +192,7 @@ class Comments(Base):
return self.text
def __repr__(self):
return u"<Comments({0})>".format(self.text)
return "<Comments({0})>".format(self.text)
class Tags(Base):
@@ -204,7 +208,7 @@ class Tags(Base):
return self.name
def __repr__(self):
return u"<Tags('{0})>".format(self.name)
return "<Tags('{0})>".format(self.name)
class Authors(Base):
@@ -224,7 +228,7 @@ class Authors(Base):
return self.name
def __repr__(self):
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
return "<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
class Series(Base):
@@ -242,7 +246,7 @@ class Series(Base):
return self.name
def __repr__(self):
return u"<Series('{0},{1}')>".format(self.name, self.sort)
return "<Series('{0},{1}')>".format(self.name, self.sort)
class Ratings(Base):
@@ -258,7 +262,7 @@ class Ratings(Base):
return self.rating
def __repr__(self):
return u"<Ratings('{0}')>".format(self.rating)
return "<Ratings('{0}')>".format(self.rating)
class Languages(Base):
@@ -277,7 +281,7 @@ class Languages(Base):
return self.lang_code
def __repr__(self):
return u"<Languages('{0}')>".format(self.lang_code)
return "<Languages('{0}')>".format(self.lang_code)
class Publishers(Base):
@@ -295,7 +299,7 @@ class Publishers(Base):
return self.name
def __repr__(self):
return u"<Publishers('{0},{1}')>".format(self.name, self.sort)
return "<Publishers('{0},{1}')>".format(self.name, self.sort)
class Data(Base):
@@ -319,7 +323,16 @@ class Data(Base):
return self.name
def __repr__(self):
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
return "<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
class Metadata_Dirtied(Base):
__tablename__ = 'metadata_dirtied'
id = Column(Integer, primary_key=True, autoincrement=True)
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
def __init__(self, book):
self.book = book
class Books(Base):
@@ -364,7 +377,7 @@ class Books(Base):
self.has_cover = (has_cover != None)
def __repr__(self):
return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
return "<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
self.timestamp, self.pubdate, self.series_index,
self.last_modified, self.path, self.has_cover)
@@ -390,6 +403,33 @@ class CustomColumns(Base):
display_dict = json.loads(self.display)
return display_dict
def to_json(self, value, extra, sequence):
content = dict()
content['table'] = "custom_column_" + str(self.id)
content['column'] = "value"
content['datatype'] = self.datatype
content['is_multiple'] = None if not self.is_multiple else "|"
content['kind'] = "field"
content['name'] = self.name
content['search_terms'] = ['#' + self.label]
content['label'] = self.label
content['colnum'] = self.id
content['display'] = self.get_display_dict()
content['is_custom'] = True
content['is_category'] = self.datatype in ['text', 'rating', 'enumeration', 'series']
content['link_column'] = "value"
content['category_sort'] = "value"
content['is_csp'] = False
content['is_editable'] = self.editable
content['rec_index'] = sequence + 22 # toDo why ??
if isinstance(value, datetime):
content['#value#'] = {"__class__": "datetime.datetime", "__value__": value.strftime("%Y-%m-%dT%H:%M:%S+00:00")}
else:
content['#value#'] = value
content['#extra#'] = extra
content['is_multiple2'] = {} if not self.is_multiple else {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}
return json.dumps(content, ensure_ascii=False)
class AlchemyEncoder(json.JSONEncoder):
@@ -641,6 +681,18 @@ class CalibreDB:
def get_book_format(self, book_id, file_format):
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
def set_metadata_dirty(self, book_id):
if not self.session.query(Metadata_Dirtied).filter(Metadata_Dirtied.book == book_id).one_or_none():
self.session.add(Metadata_Dirtied(book_id))
def delete_dirty_metadata(self, book_id):
try:
self.session.query(Metadata_Dirtied).filter(Metadata_Dirtied.book == book_id).delete()
self.session.commit()
except (OperationalError) as e:
self.session.rollback()
log.error("Database error: {}".format(e))
# Language and content filters for displaying in the UI
def common_filters(self, allow_show_archived=False, return_all_languages=False):
if not allow_show_archived:
+1 -1
View File
@@ -65,7 +65,7 @@ def send_debug():
file_list.remove(element)
memory_zip = BytesIO()
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr('settings.txt', json.dumps(config.toDict(), sort_keys=True, indent=2))
zf.writestr('settings.txt', json.dumps(config.to_dict(), sort_keys=True, indent=2))
zf.writestr('libs.txt', json.dumps(collect_stats(), sort_keys=True, indent=2, cls=lazyEncoder))
for fp in file_list:
zf.write(fp, os.path.basename(fp))
+39 -32
View File
@@ -38,7 +38,7 @@ from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError, IntegrityError
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
from sqlalchemy.orm.exc import StaleDataError
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
@@ -107,7 +107,7 @@ def edit_book(book_id):
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
# Book not found
if not book:
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error")
return redirect(url_for("web.index"))
@@ -151,7 +151,7 @@ def edit_book(book_id):
if to_save.get("cover_url", None):
if not current_user.role_upload():
edit_error = True
flash(_(u"User has no rights to upload cover"), category="error")
flash(_("User has no rights to upload cover"), category="error")
if to_save["cover_url"].endswith('/static/generic_cover.jpg'):
book.has_cover = 0
else:
@@ -203,6 +203,7 @@ def edit_book(book_id):
if modify_date:
book.last_modified = datetime.utcnow()
kobo_sync_status.remove_synced_book(edited_books_id, all=True)
calibre_db.set_metadata_dirty(book.id)
calibre_db.session.merge(book)
calibre_db.session.commit()
@@ -222,10 +223,10 @@ def edit_book(book_id):
calibre_db.session.rollback()
flash(str(e), category="error")
return redirect(url_for('web.show_book', book_id=book.id))
except (OperationalError, IntegrityError, StaleDataError) as e:
except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e:
log.error_or_exception("Database error: {}".format(e))
calibre_db.session.rollback()
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error")
return redirect(url_for('web.show_book', book_id=book.id))
except Exception as ex:
log.error_or_exception(ex)
@@ -277,6 +278,8 @@ def upload():
move_coverfile(meta, db_book)
if modify_date:
calibre_db.set_metadata_dirty(book_id)
# save data to database, reread data
calibre_db.session.commit()
@@ -285,7 +288,7 @@ def upload():
if error:
flash(error, category="error")
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
upload_text = N_(u"File %(file)s uploaded", file=link)
upload_text = N_("File %(file)s uploaded", file=link)
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
helper.add_book_to_thumbnail_cache(book_id)
@@ -299,7 +302,8 @@ def upload():
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
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')
@@ -312,7 +316,7 @@ def convert_bookformat(book_id):
book_format_to = request.form.get('book_format_to', None)
if (book_format_from is None) or (book_format_to is None):
flash(_(u"Source or destination format for conversion missing"), category="error")
flash(_("Source or destination format for conversion missing"), category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
@@ -320,11 +324,11 @@ def convert_bookformat(book_id):
book_format_to.upper(), current_user.name)
if rtn is None:
flash(_(u"Book successfully queued for converting to %(book_format)s",
flash(_("Book successfully queued for converting to %(book_format)s",
book_format=book_format_to),
category="success")
else:
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
flash(_("There was an error converting this book: %(res)s", res=rtn), category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
@@ -448,7 +452,7 @@ def edit_list_book(param):
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
ret = Response(json.dumps({'success': False,
'msg': 'Database error: {}'.format(e.orig)}),
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}),
mimetype='application/json')
return ret
@@ -555,11 +559,12 @@ def table_xchange_author_title():
renamed_author=renamed)
if modify_date:
book.last_modified = datetime.utcnow()
calibre_db.set_metadata_dirty(book.id)
try:
calibre_db.session.commit()
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: %s", e)
log.error_or_exception("Database error: {}".format(e))
return json.dumps({'success': False})
if config.config_use_google_drive:
@@ -569,9 +574,9 @@ def table_xchange_author_title():
def merge_metadata(to_save, meta):
if to_save.get('author_name', "") == _(u'Unknown'):
if to_save.get('author_name', "") == _('Unknown'):
to_save['author_name'] = ''
if to_save.get('book_title', "") == _(u'Unknown'):
if to_save.get('book_title', "") == _('Unknown'):
to_save['book_title'] = ''
for s_field, m_field in [
('tags', 'tags'), ('author_name', 'author'), ('series', 'series'),
@@ -607,7 +612,7 @@ def prepare_authors(authr):
# we have all author names now
if input_authors == ['']:
input_authors = [_(u'Unknown')] # prevent empty Author
input_authors = [_('Unknown')] # prevent empty Author
renamed = list()
for in_aut in input_authors:
@@ -624,11 +629,11 @@ def prepare_authors(authr):
def prepare_authors_on_upload(title, authr):
if title != _(u'Unknown') and authr != _(u'Unknown'):
if title != _('Unknown') and authr != _('Unknown'):
entry = calibre_db.check_exists_book(authr, title)
if entry:
log.info("Uploaded book probably exists in library")
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
flash(_("Uploaded book probably exists in the library, consider to change before upload new: ")
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
input_authors, renamed = prepare_authors(authr)
@@ -683,7 +688,7 @@ def create_book_on_upload(modify_date, meta):
modify_date |= edit_book_languages(meta.languages, db_book, upload_mode=True, invalid=invalid)
if invalid:
for lang in invalid:
flash(_(u"'%(langname)s' is not a valid language", langname=lang), category="warning")
flash(_("'%(langname)s' is not a valid language", langname=lang), category="warning")
# handle tags
modify_date |= edit_book_tags(meta.tags, db_book)
@@ -733,7 +738,7 @@ def file_handling_on_upload(requested_file):
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir",
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 meta, None
@@ -753,7 +758,7 @@ def move_coverfile(meta, db_book):
os.unlink(meta.cover)
except OSError as e:
log.error("Failed to move cover file %s: %s", new_cover_path, e)
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path,
flash(_("Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path,
error=e),
category="error")
@@ -767,7 +772,7 @@ def delete_whole_book(book_id, book):
# check if only this book links to:
# author, language, series, tags, custom columns
modify_database_object([u''], book.authors, db.Authors, calibre_db.session, 'author')
modify_database_object([''], book.authors, db.Authors, calibre_db.session, 'author')
modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags')
modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series')
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
@@ -888,7 +893,7 @@ def render_edit_book(book_id):
cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
if not book:
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error")
return redirect(url_for("web.index"))
@@ -923,7 +928,7 @@ def render_edit_book(book_id):
if kepub_possible:
allowed_conversion_formats.append('kepub')
return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
title=_(u"edit metadata"), page="editbook",
title=_("edit metadata"), page="editbook",
conversion_formats=allowed_conversion_formats,
config=config,
source_formats=valid_source_formats)
@@ -1008,7 +1013,7 @@ def edit_book_languages(languages, book, upload_mode=False, invalid=None):
if isinstance(invalid, list):
invalid.append(lang)
else:
raise ValueError(_(u"'%(langname)s' is not a valid language", langname=lang))
raise ValueError(_("'%(langname)s' is not a valid language", langname=lang))
# ToDo: Not working correct
if upload_mode and len(input_l) == 1:
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
@@ -1119,9 +1124,10 @@ def edit_cc_data(book_id, book, to_save, cc):
cc_db_value = None
if to_save[cc_string].strip():
if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]:
changed, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
change, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
else:
changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
change, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
changed |= change
else:
if cc_db_value is not None:
# remove old cc_val
@@ -1150,7 +1156,7 @@ def upload_single_file(file_request, book, book_id):
# check for empty request
if requested_file.filename != '':
if not current_user.role_upload():
flash(_(u"User has no rights to upload additional file formats"), category="error")
flash(_("User has no rights to upload additional file formats"), category="error")
return False
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
@@ -1171,12 +1177,12 @@ def upload_single_file(file_request, book, book_id):
try:
os.makedirs(filepath)
except OSError:
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
flash(_("Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
return False
try:
requested_file.save(saved_filename)
except OSError:
flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error")
flash(_("Failed to store file %(file)s.", file=saved_filename), category="error")
return False
file_size = os.path.getsize(saved_filename)
@@ -1194,12 +1200,13 @@ def upload_single_file(file_request, book, book_id):
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
category="error")
return False # return redirect(url_for('web.show_book', book_id=book.id))
# Queue uploader info
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
upload_text = N_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
upload_text = N_("File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
return uploader.process(
@@ -1214,7 +1221,7 @@ def upload_cover(cover_request, book):
# check for empty request
if requested_file.filename != '':
if not current_user.role_upload():
flash(_(u"User has no rights to upload cover"), category="error")
flash(_("User has no rights to upload cover"), category="error")
return False
ret, message = helper.save_cover(requested_file, book.path)
if ret is True:
+74 -48
View File
@@ -21,6 +21,7 @@ import zipfile
from lxml import etree
from . import isoLanguages, cover
from . import config
from .helper import split_authors
from .constants import BookMeta
@@ -28,18 +29,40 @@ from .constants import BookMeta
def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name):
if cover_file is None:
return None
else:
cf = extension = None
zip_cover_path = os.path.join(cover_path, cover_file).replace('\\', '/')
prefix = os.path.splitext(tmp_file_name)[0]
tmp_cover_name = prefix + '.' + os.path.basename(zip_cover_path)
ext = os.path.splitext(tmp_cover_name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in cover.COVER_EXTENSIONS:
cf = zip_file.read(zip_cover_path)
return cover.cover_processing(tmp_file_name, cf, extension)
cf = extension = None
zip_cover_path = os.path.join(cover_path, cover_file).replace('\\', '/')
prefix = os.path.splitext(tmp_file_name)[0]
tmp_cover_name = prefix + '.' + os.path.basename(zip_cover_path)
ext = os.path.splitext(tmp_cover_name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in cover.COVER_EXTENSIONS:
cf = zip_file.read(zip_cover_path)
return cover.cover_processing(tmp_file_name, cf, extension)
def get_epub_layout(book, book_data):
ns = {
'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
'pkg': 'http://www.idpf.org/2007/opf',
}
file_path = os.path.normpath(os.path.join(config.config_calibre_dir, book.path, book_data.name + "." + book_data.format.lower()))
epubZip = zipfile.ZipFile(file_path)
txt = epubZip.read('META-INF/container.xml')
tree = etree.fromstring(txt)
cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
cf = epubZip.read(cfname)
tree = etree.fromstring(cf)
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0]
layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns)
if len(layout) == 0:
return None
else:
return layout[0]
def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
@@ -80,13 +103,13 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
if epub_metadata['subject'] == 'Unknown':
epub_metadata['subject'] = ''
if epub_metadata['publisher'] == u'Unknown':
if epub_metadata['publisher'] == 'Unknown':
epub_metadata['publisher'] = ''
if epub_metadata['date'] == u'Unknown':
if epub_metadata['date'] == 'Unknown':
epub_metadata['date'] = ''
if epub_metadata['description'] == u'Unknown':
if epub_metadata['description'] == 'Unknown':
description = tree.xpath("//*[local-name() = 'description']/text()")
if len(description) > 0:
epub_metadata['description'] = description
@@ -102,11 +125,14 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
identifiers = []
for node in p.xpath('dc:identifier', namespaces=ns):
identifier_name=node.attrib.values()[-1];
identifier_value=node.text;
if identifier_name in ('uuid','calibre'):
continue;
identifiers.append( [identifier_name, identifier_value] )
try:
identifier_name = node.attrib.values()[-1]
except IndexError:
continue
identifier_value = node.text
if identifier_name in ('uuid', 'calibre') or identifier_value is None:
continue
identifiers.append([identifier_name, identifier_value])
if not epub_metadata['title']:
title = original_file_name
@@ -131,40 +157,40 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
cover_section = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
cover_file = None
# if len(cover_section) > 0:
for cs in cover_section:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file:
break
if not cover_file:
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(meta_cover) > 0:
return cover_file
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(meta_cover) > 0:
cover_section = tree.xpath(
"/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns)
if not cover_section:
cover_section = tree.xpath(
"/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns)
if not cover_section:
cover_section = tree.xpath(
"/pkg:package/pkg:manifest/pkg:item[@properties='" + meta_cover[0] + "']/@href", namespaces=ns)
"/pkg:package/pkg:manifest/pkg:item[@properties='" + meta_cover[0] + "']/@href", namespaces=ns)
else:
cover_section = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns)
cover_file = None
for cs in cover_section:
if cs.endswith('.xhtml') or cs.endswith('.html'):
markup = epub_zip.read(os.path.join(cover_path, cs))
markup_tree = etree.fromstring(markup)
# no matter xhtml or html with no namespace
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
# Alternative image source
if not len(img_src):
img_src = markup_tree.xpath("//attribute::*[contains(local-name(), 'href')]")
if len(img_src):
# img_src maybe start with "../"" so fullpath join then relpath to cwd
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(cover_path, cover_section[0])),
img_src[0]))
cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
else:
cover_section = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns)
for cs in cover_section:
filetype = cs.rsplit('.', 1)[-1]
if filetype == "xhtml" or filetype == "html": # if cover is (x)html format
markup = epub_zip.read(os.path.join(cover_path, cs))
markup_tree = etree.fromstring(markup)
# no matter xhtml or html with no namespace
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
# Alternative image source
if not len(img_src):
img_src = markup_tree.xpath("//attribute::*[contains(local-name(), 'href')]")
if len(img_src):
# img_src maybe start with "../"" so fullpath join then relpath to cwd
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(cover_path, cover_section[0])),
img_src[0]))
cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
else:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file: break
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file:
break
return cover_file
+7 -7
View File
@@ -38,19 +38,19 @@ def get_fb2_info(tmp_file_path, original_file_extension):
if len(last_name):
last_name = last_name[0]
else:
last_name = u''
last_name = ''
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
if len(middle_name):
middle_name = middle_name[0]
else:
middle_name = u''
middle_name = ''
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
if len(first_name):
first_name = first_name[0]
else:
first_name = u''
return (first_name + u' '
+ middle_name + u' '
first_name = ''
return (first_name + ' '
+ middle_name + ' '
+ last_name)
author = str(", ".join(map(get_author, authors)))
@@ -59,12 +59,12 @@ def get_fb2_info(tmp_file_path, original_file_extension):
if len(title):
title = str(title[0])
else:
title = u''
title = ''
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
if len(description):
description = str(description[0])
else:
description = u''
description = ''
return BookMeta(
file_path=tmp_file_path,
+4 -4
View File
@@ -55,7 +55,7 @@ def authenticate_google_drive():
try:
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
except gdriveutils.InvalidConfigError:
flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'),
flash(_('Google Drive setup not completed, try to deactivate and activate Google Drive again'),
category="error")
return redirect(url_for('web.index'))
return redirect(authUrl)
@@ -91,9 +91,9 @@ def watch_gdrive():
config.save()
except HttpError as e:
reason=json.loads(e.content)['error']['errors'][0]
if reason['reason'] == u'push.webhookUrlUnauthorized':
flash(_(u'Callback domain is not verified, '
u'please follow steps to verify domain in google developer console'), category="error")
if reason['reason'] == 'push.webhookUrlUnauthorized':
flash(_('Callback domain is not verified, '
'please follow steps to verify domain in google developer console'), category="error")
else:
flash(reason['message'], category="error")
+31 -22
View File
@@ -174,30 +174,12 @@ class PermissionAdded(Base):
return str(self.gdrive_id)
def migrate():
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
PermissionAdded.__table__.create(bind = engine)
for sql in session.execute(text("select sql from sqlite_master where type='table'")):
if 'CREATE TABLE gdrive_ids' in sql[0]:
currUniqueConstraint = 'UNIQUE (gdrive_id)'
if currUniqueConstraint in sql[0]:
sql=sql[0].replace(currUniqueConstraint, 'UNIQUE (gdrive_id, path)')
sql=sql.replace(GdriveId.__tablename__, GdriveId.__tablename__ + '2')
session.execute(sql)
session.execute("INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, "
"gdrive_id, path FROM gdrive_ids;")
session.commit()
session.execute('DROP TABLE %s' % 'gdrive_ids')
session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids')
break
if not os.path.exists(cli_param.gd_path):
try:
Base.metadata.create_all(engine)
except Exception as ex:
log.error("Error connect to database: {} - {}".format(cli_param.gd_path, ex))
raise
migrate()
def getDrive(drive=None, gauth=None):
@@ -344,7 +326,7 @@ def getFileFromEbooksFolder(path, fileName):
def moveGdriveFileRemote(origin_file_id, new_title):
origin_file_id['title']= new_title
origin_file_id['title'] = new_title
origin_file_id.Upload()
@@ -422,7 +404,7 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
driveFile.Upload()
def uploadFileToEbooksFolder(destFile, f):
def uploadFileToEbooksFolder(destFile, f, string=False):
drive = getDrive(Gdrive.Instance().drive)
parent = getEbooksFolder(drive)
splitDir = destFile.split('/')
@@ -435,7 +417,10 @@ def uploadFileToEbooksFolder(destFile, f):
else:
driveFile = drive.CreateFile({'title': x,
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], })
driveFile.SetContentFile(f)
if not string:
driveFile.SetContentFile(f)
else:
driveFile.SetContentString(f)
driveFile.Upload()
else:
existing_Folder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
@@ -556,7 +541,7 @@ def updateGdriveCalibreFromLocal():
# update gdrive.db on edit of books title
def updateDatabaseOnEdit(ID,newPath):
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/'
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
if storedPathName:
storedPathName.path = sqlCheckPath
@@ -578,6 +563,7 @@ def deleteDatabaseEntry(ID):
# Gets cover file from gdrive
# ToDo: Check is this right everyone get read permissions on cover files?
def get_cover_via_gdrive(cover_path):
df = getFileFromEbooksFolder(cover_path, 'cover.jpg')
if df:
@@ -600,6 +586,29 @@ def get_cover_via_gdrive(cover_path):
else:
return None
# Gets cover file from gdrive
def get_metadata_backup_via_gdrive(metadata_path):
df = getFileFromEbooksFolder(metadata_path, 'metadata.opf')
if df:
if not session.query(PermissionAdded).filter(PermissionAdded.gdrive_id == df['id']).first():
df.GetPermissions()
df.InsertPermission({
'type': 'anyone',
'value': 'anyone',
'role': 'writer', # ToDo needs write access
'withLink': True})
permissionAdded = PermissionAdded()
permissionAdded.gdrive_id = df['id']
session.add(permissionAdded)
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
return df.metadata.get('webContentLink')
else:
return None
# Creates chunks for downloading big files
def partial(total_byte_len, part_size_limit):
s = []
Executable → Regular
+120 -70
View File
@@ -18,8 +18,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import random
import io
import sys
import mimetypes
import re
import shutil
@@ -32,6 +32,7 @@ import unidecode
from flask import send_from_directory, make_response, redirect, abort, url_for
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
from flask_login import current_user
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
from sqlalchemy.exc import InvalidRequestError, OperationalError
@@ -58,6 +59,7 @@ from .subproc_wrapper import process_wait
from .services.worker import WorkerThread
from .tasks.mail import TaskEmail
from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails
from .tasks.metadata_backup import TaskBackupMetadata
log = logger.create()
@@ -76,29 +78,29 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
book = calibre_db.get_book(book_id)
data = calibre_db.get_book_format(book.id, old_book_format)
if not data:
error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
error_message = _("%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
log.error("convert_book_format: %s", error_message)
return error_message
file_path = os.path.join(calibre_path, book.path, data.name)
if config.config_use_google_drive:
if not gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()):
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
error_message = _("%(format)s not found on Google Drive: %(fn)s",
format=old_book_format, fn=data.name + "." + old_book_format.lower())
return error_message
else:
if not os.path.exists(file_path + "." + old_book_format.lower()):
error_message = _(u"%(format)s not found: %(fn)s",
error_message = _("%(format)s not found: %(fn)s",
format=old_book_format, fn=data.name + "." + old_book_format.lower())
return error_message
# read settings and append converter task to queue
if ereader_mail:
settings = config.get_mail_settings()
settings['subject'] = _('Send to E-Reader') # pretranslate Subject for e-mail
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
settings['subject'] = _('Send to eReader') # pretranslate Subject for Email
settings['body'] = _('This Email has been sent via Calibre-Web.')
else:
settings = dict()
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss
txt = u"{} -> {}: {}".format(
txt = "{} -> {}: {}".format(
old_book_format.upper(),
new_book_format.upper(),
link)
@@ -110,30 +112,30 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
# Texts are not lazy translated as they are supposed to get send out as is
def send_test_mail(ereader_mail, user_name):
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
config.get_mail_settings(), ereader_mail, N_(u"Test e-mail"),
_(u'This e-mail has been sent via Calibre-Web.')))
WorkerThread.add(user_name, TaskEmail(_('Calibre-Web Test Email'), None, None,
config.get_mail_settings(), ereader_mail, N_("Test Email"),
_('This Email has been sent via Calibre-Web.')))
return
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
def send_registration_mail(e_mail, user_name, default_password, resend=False):
txt = "Hello %s!\r\n" % user_name
txt = "Hi %s!\r\n" % user_name
if not resend:
txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
txt += "Please log in to your account using the following information:\r\n"
txt += "User name: %s\r\n" % user_name
txt += "Your account at Calibre-Web has been created.\r\n"
txt += "Please log in using the following information:\r\n"
txt += "Username: %s\r\n" % user_name
txt += "Password: %s\r\n" % default_password
txt += "Don't forget to change your password after first login.\r\n"
txt += "Sincerely\r\n\r\n"
txt += "Your Calibre-Web team"
txt += "Don't forget to change your password after your first login.\r\n"
txt += "Regards,\r\n\r\n"
txt += "Calibre-Web"
WorkerThread.add(None, TaskEmail(
subject=_(u'Get Started with Calibre-Web'),
subject=_('Get Started with Calibre-Web'),
filepath=None,
attachment=None,
settings=config.get_mail_settings(),
recipient=e_mail,
task_message=N_(u"Registration e-mail for user: %(name)s", name=user_name),
task_message=N_("Registration Email for user: %(name)s", name=user_name),
text=txt
))
return
@@ -144,13 +146,13 @@ def check_send_to_ereader_with_converter(formats):
if 'MOBI' in formats and 'EPUB' not in formats:
book_formats.append({'format': 'Epub',
'convert': 1,
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
'text': _('Convert %(orig)s to %(format)s and send to eReader',
orig='Mobi',
format='Epub')})
if 'AZW3' in formats and 'EPUB' not in formats:
book_formats.append({'format': 'Epub',
'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
'text': _('Convert %(orig)s to %(format)s and send to eReader',
orig='Azw3',
format='Epub')})
return book_formats
@@ -158,7 +160,7 @@ def check_send_to_ereader_with_converter(formats):
def check_send_to_ereader(entry):
"""
returns all available book formats for sending to E-Reader
returns all available book formats for sending to eReader
"""
formats = list()
book_formats = list()
@@ -169,24 +171,20 @@ def check_send_to_ereader(entry):
if 'EPUB' in formats:
book_formats.append({'format': 'Epub',
'convert': 0,
'text': _('Send %(format)s to E-Reader', format='Epub')})
if 'MOBI' in formats:
book_formats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to E-Reader', format='Mobi')})
'text': _('Send %(format)s to eReader', format='Epub')})
if 'PDF' in formats:
book_formats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to E-Reader', format='Pdf')})
'text': _('Send %(format)s to eReader', format='Pdf')})
if 'AZW' in formats:
book_formats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to E-Reader', format='Azw')})
'text': _('Send %(format)s to eReader', format='Azw')})
if config.config_converterpath:
book_formats.extend(check_send_to_ereader_with_converter(formats))
return book_formats
else:
log.error(u'Cannot find book entry %d', entry.id)
log.error('Cannot find book entry %d', entry.id)
return None
@@ -203,30 +201,30 @@ def check_read_formats(entry):
# Files are processed in the following order/priority:
# 1: If Mobi file is existing, it's directly send to E-Reader email,
# 2: If Epub file is existing, it's converted and send to E-Reader email,
# 3: If Pdf file is existing, it's directly send to E-Reader email
# 1: If epub file is existing, it's directly send to eReader email,
# 2: If mobi file is existing, it's converted and send to eReader email,
# 3: If Pdf file is existing, it's directly send to eReader email
def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id):
"""Send email with attachments"""
book = calibre_db.get_book(book_id)
if convert == 1:
# returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'epub', book_format.lower(), user_id, ereader_mail)
return convert_book_format(book_id, calibrepath, 'mobi', book_format.lower(), user_id, ereader_mail)
if convert == 2:
# returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, ereader_mail)
return convert_book_format(book_id, calibrepath, 'azw3', book_format.lower(), user_id, ereader_mail)
for entry in iter(book.data):
if entry.format.upper() == book_format.upper():
converted_file_name = entry.name + '.' + book_format.lower()
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
email_text = N_(u"%(book)s send to E-Reader", book=link)
WorkerThread.add(user_id, TaskEmail(_(u"Send to E-Reader"), book.path, converted_file_name,
email_text = N_("%(book)s send to eReader", book=link)
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
config.get_mail_settings(), ereader_mail,
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
email_text, _('This Email has been sent via Calibre-Web.')))
return
return _(u"The requested file could not be read. Maybe wrong permissions?")
return _("The requested file could not be read. Maybe wrong permissions?")
def get_valid_filename(value, replace_whitespace=True, chars=128):
@@ -234,16 +232,16 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
Returns the given string converted to a string that can be used for a clean
filename. Limits num characters to 128 max.
"""
if value[-1:] == u'.':
value = value[:-1]+u'_'
if value[-1:] == '.':
value = value[:-1]+'_'
value = value.replace("/", "_").replace(":", "_").strip('\0')
if config.config_unicode_filename:
value = (unidecode.unidecode(value))
if replace_whitespace:
# *+:\"/<>? are replaced by _
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
value = re.sub(r'[*+:\\\"/<>?]+', '_', value, flags=re.U)
# pipe has to be replaced with comma
value = re.sub(r'[|]+', u',', value, flags=re.U)
value = re.sub(r'[|]+', ',', value, flags=re.U)
value = value.encode('utf-8')[:chars].decode('utf-8', errors='ignore').strip()
@@ -340,7 +338,7 @@ def edit_book_read_status(book_id, read_status=None):
return "Custom Column No.{} does not exist in calibre database".format(config.config_read_column)
except (OperationalError, InvalidRequestError) as ex:
calibre_db.session.rollback()
log.error(u"Read status could not set: {}".format(ex))
log.error("Read status could not set: {}".format(ex))
return _("Read status could not set: {}".format(ex.orig))
return ""
@@ -415,8 +413,8 @@ def clean_author_database(renamed_author, calibre_path="", local_book=None, gdri
g_file = gd.getFileFromEbooksFolder(all_new_path,
file_format.name + '.' + file_format.format.lower())
if g_file:
gd.moveGdriveFileRemote(g_file, all_new_name + u'.' + file_format.format.lower())
gd.updateDatabaseOnEdit(g_file['id'], all_new_name + u'.' + file_format.format.lower())
gd.moveGdriveFileRemote(g_file, all_new_name + '.' + file_format.format.lower())
gd.updateDatabaseOnEdit(g_file['id'], all_new_name + '.' + file_format.format.lower())
else:
log.error("File {} not found on gdrive"
.format(all_new_path, file_format.name + '.' + file_format.format.lower()))
@@ -509,25 +507,25 @@ def update_dir_structure_gdrive(book_id, first_author, renamed_author):
authordir = book.path.split('/')[0]
titledir = book.path.split('/')[1]
new_authordir = rename_all_authors(first_author, renamed_author, gdrive=True)
new_titledir = get_valid_filename(book.title, chars=96) + u" (" + str(book_id) + u")"
new_titledir = get_valid_filename(book.title, chars=96) + " (" + str(book_id) + ")"
if titledir != new_titledir:
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if g_file:
gd.moveGdriveFileRemote(g_file, new_titledir)
book.path = book.path.split('/')[0] + u'/' + new_titledir
book.path = book.path.split('/')[0] + '/' + new_titledir
gd.updateDatabaseOnEdit(g_file['id'], book.path) # only child folder affected
else:
return _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
return _('File %(file)s not found on Google Drive', file=book.path) # file not found
if authordir != new_authordir and authordir not in renamed_author:
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
if g_file:
gd.moveGdriveFolderRemote(g_file, new_authordir)
book.path = new_authordir + u'/' + book.path.split('/')[1]
book.path = new_authordir + '/' + book.path.split('/')[1]
gd.updateDatabaseOnEdit(g_file['id'], book.path)
else:
return _(u'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
# change location in database to new author/title path
book.path = os.path.join(new_authordir, new_titledir).replace('\\', '/')
@@ -599,7 +597,7 @@ def delete_book_gdrive(book, book_format):
gd.deleteDatabaseEntry(g_file['id'])
g_file.Trash()
else:
error = _(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
error = _('Book path %(path)s not found on Google Drive', path=book.path) # file not found
return error is None, error
@@ -611,7 +609,7 @@ def reset_password(user_id):
if not config.get_mail_server_configured():
return 2, None
try:
password = generate_random_password()
password = generate_random_password(config.config_password_min_length)
existing_user.password = generate_password_hash(password)
ub.session.commit()
send_registration_mail(existing_user.email, existing_user.name, password, True)
@@ -620,11 +618,35 @@ def reset_password(user_id):
ub.session.rollback()
return 0, None
def generate_random_password(min_length):
min_length = max(8, min_length) - 4
random_source = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
# select 1 lowercase
s = "abcdefghijklmnopqrstuvwxyz"
password = [s[c % len(s)] for c in os.urandom(1)]
# select 1 uppercase
s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
password.extend([s[c % len(s)] for c in os.urandom(1)])
# select 1 digit
s = "01234567890"
password.extend([s[c % len(s)] for c in os.urandom(1)])
# select 1 special symbol
s = "!@#$%&*()?"
password.extend([s[c % len(s)] for c in os.urandom(1)])
def generate_random_password():
# generate other characters
password.extend([random_source[c % len(random_source)] for c in os.urandom(min_length)])
# password_list = list(password)
# shuffle all characters
random.SystemRandom().shuffle(password)
return ''.join(password)
'''def generate_random_password(min_length):
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
passlen = 8
return "".join(s[c % len(s)] for c in os.urandom(passlen))
passlen = min_length
return "".join(s[c % len(s)] for c in os.urandom(passlen))'''
def uniq(inpt):
@@ -639,28 +661,47 @@ def uniq(inpt):
def check_email(email):
email = valid_email(email)
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
log.error(u"Found an existing account for this e-mail address")
raise Exception(_(u"Found an existing account for this e-mail address"))
log.error("Found an existing account for this Email address")
raise Exception(_("Found an existing account for this Email address"))
return email
def check_username(username):
username = username.strip()
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
log.error(u"This username is already taken")
raise Exception(_(u"This username is already taken"))
log.error("This username is already taken")
raise Exception(_("This username is already taken"))
return username
def valid_email(email):
email = email.strip()
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
email):
log.error(u"Invalid e-mail address format")
raise Exception(_(u"Invalid e-mail address format"))
# if email is not deleted
if email:
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
email):
log.error("Invalid Email address format")
raise Exception(_("Invalid Email address format"))
return email
def valid_password(check_password):
if config.config_password_policy:
verify = ""
if config.config_password_min_length > 0:
verify += "^(?=.{" + str(config.config_password_min_length) + ",}$)"
if config.config_password_number:
verify += "(?=.*?\d)"
if config.config_password_lower:
verify += "(?=.*?[a-z])"
if config.config_password_upper:
verify += "(?=.*?[A-Z])"
if config.config_password_special:
verify += "(?=.*?[^A-Za-z\s0-9])"
match = re.match(verify, check_password)
if not match:
raise Exception(_("Password doesn't comply with password validation rules"))
return check_password
# ################################# External interface #################################
@@ -683,7 +724,8 @@ def update_dir_structure(book_id,
def delete_book(book, calibrepath, book_format):
if not book_format:
clear_cover_thumbnail_cache(book.id) ## here it breaks
clear_cover_thumbnail_cache(book.id) ## here it breaks
calibre_db.delete_dirty_metadata(book.id)
if config.config_use_google_drive:
return delete_book_gdrive(book, book_format)
else:
@@ -833,8 +875,8 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
try:
os.makedirs(filepath)
except OSError:
log.error(u"Failed to create path for cover")
return False, _(u"Failed to create path for cover")
log.error("Failed to create path for cover")
return False, _("Failed to create path for cover")
try:
# upload of jgp file without wand
if isinstance(img, requests.Response):
@@ -849,8 +891,8 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
# upload of jpg/png... from hdd
img.save(os.path.join(filepath, saved_filename))
except (IOError, OSError):
log.error(u"Cover-file is not a valid image file, or could not be stored")
return False, _(u"Cover-file is not a valid image file, or could not be stored")
log.error("Cover-file is not a valid image file, or could not be stored")
return False, _("Cover-file is not a valid image file, or could not be stored")
return True, None
@@ -1029,3 +1071,11 @@ def add_book_to_thumbnail_cache(book_id):
def update_thumbnail_cache():
if config.schedule_generate_book_covers:
WorkerThread.add(None, TaskGenerateCoverThumbnails())
def set_all_metadata_dirty():
WorkerThread.add(None, TaskBackupMetadata(export_language=get_locale(),
translated_title=_("Cover"),
set_dirty=True,
task_message=N_("Queue all books for metadata backup")),
hidden=False)
File diff suppressed because it is too large Load Diff
+32 -18
View File
@@ -21,6 +21,7 @@ import base64
import datetime
import os
import uuid
import zipfile
from time import gmtime, strftime
import json
from urllib.parse import unquote
@@ -45,6 +46,8 @@ import requests
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
from . import isoLanguages
from .epub import get_epub_layout
from .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL
from .helper import get_download_link
from .services import SyncToken as SyncToken
@@ -355,7 +358,7 @@ def HandleMetadataRequest(book_uuid):
log.info("Kobo library metadata request received for book %s" % book_uuid)
book = calibre_db.get_book_by_uuid(book_uuid)
if not book or not book.data:
log.info(u"Book %s not found in database", book_uuid)
log.info("Book %s not found in database", book_uuid)
return redirect_or_proxy_request()
metadata = get_metadata(book)
@@ -443,6 +446,12 @@ def get_seriesindex(book):
return book.series_index or 1
def get_language(book):
if not book.languages:
return 'en'
return isoLanguages.get(part3=book.languages[0].lang_code).part1
def get_metadata(book):
download_urls = []
kepub = [data for data in book.data if data.format == 'KEPUB']
@@ -452,16 +461,21 @@ def get_metadata(book):
continue
for kobo_format in KOBO_FORMATS[book_data.format]:
# log.debug('Id: %s, Format: %s' % (book.id, kobo_format))
download_urls.append(
{
"Format": kobo_format,
"Size": book_data.uncompressed_size,
"Url": get_download_url_for_book(book, book_data.format),
# The Kobo forma accepts platforms: (Generic, Android)
"Platform": "Generic",
# "DrmType": "None", # Not required
}
)
try:
if get_epub_layout(book, book_data) == 'pre-paginated':
kobo_format = 'EPUB3FL'
download_urls.append(
{
"Format": kobo_format,
"Size": book_data.uncompressed_size,
"Url": get_download_url_for_book(book, book_data.format),
# The Kobo forma accepts platforms: (Generic, Android)
"Platform": "Generic",
# "DrmType": "None", # Not required
}
)
except (zipfile.BadZipfile, FileNotFoundError) as e:
log.error(e)
book_uuid = book.uuid
metadata = {
@@ -480,7 +494,7 @@ def get_metadata(book):
"IsInternetArchive": False,
"IsPreOrder": False,
"IsSocialEnabled": True,
"Language": "en",
"Language": get_language(book),
"PhoneticPronunciations": {},
"PublicationDate": convert_to_kobo_timestamp_string(book.pubdate),
"Publisher": {"Imprint": "", "Name": get_publisher(book), },
@@ -752,7 +766,7 @@ def create_kobo_tag(shelf):
for book_shelf in shelf.books:
book = calibre_db.get_book(book_shelf.book_id)
if not book:
log.info(u"Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
log.info("Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
continue
tag["Items"].append(
{
@@ -769,7 +783,7 @@ def create_kobo_tag(shelf):
def HandleStateRequest(book_uuid):
book = calibre_db.get_book_by_uuid(book_uuid)
if not book or not book.data:
log.info(u"Book %s not found in database", book_uuid)
log.info("Book %s not found in database", book_uuid)
return redirect_or_proxy_request()
kobo_reading_state = get_or_create_reading_state(book.id)
@@ -944,7 +958,7 @@ def HandleBookDeletionRequest(book_uuid):
log.info("Kobo book delete request received for book %s" % book_uuid)
book = calibre_db.get_book_by_uuid(book_uuid)
if not book:
log.info(u"Book %s not found in database", book_uuid)
log.info("Book %s not found in database", book_uuid)
return redirect_or_proxy_request()
book_id = book.id
@@ -958,7 +972,7 @@ def HandleBookDeletionRequest(book_uuid):
@csrf.exempt
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
def HandleUnimplementedRequest(dummy=None):
log.debug("Unimplemented Library Request received: %s", request.base_url)
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)", request.base_url)
return redirect_or_proxy_request()
@@ -970,7 +984,7 @@ def HandleUnimplementedRequest(dummy=None):
@kobo.route("/v1/user/recommendations", methods=["GET", "POST"])
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
def HandleUserRequest(dummy=None):
log.debug("Unimplemented User Request received: %s", request.base_url)
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
return redirect_or_proxy_request()
@@ -1010,7 +1024,7 @@ def handle_getests():
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
@kobo.route("/v1/deals", methods=["GET", "POST"])
def HandleProductsRequest(dummy=None):
log.debug("Unimplemented Products Request received: %s", request.base_url)
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)", request.base_url)
return redirect_or_proxy_request()
+11 -5
View File
@@ -64,11 +64,12 @@ from datetime import datetime
from os import urandom
from functools import wraps
from flask import g, Blueprint, url_for, abort, request
from flask import g, Blueprint, abort, request
from flask_login import login_user, current_user, login_required
from flask_babel import gettext as _
from flask_limiter import RateLimitExceeded
from . import logger, config, calibre_db, db, helper, ub, lm
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
from .render_template import render_title_template
log = logger.create()
@@ -112,7 +113,7 @@ def generate_auth_token(user_id):
return render_title_template(
"generate_kobo_auth_url.html",
title=_(u"Kobo Setup"),
title=_("Kobo Setup"),
auth_token=auth_token.auth_token,
warning = warning
)
@@ -151,6 +152,10 @@ def requires_kobo_auth(f):
def inner(*args, **kwargs):
auth_token = get_auth_token()
if auth_token is not None:
try:
limiter.check()
except RateLimitExceeded:
return abort(429)
user = (
ub.session.query(ub.User)
.join(ub.RemoteAuthToken)
@@ -159,7 +164,8 @@ def requires_kobo_auth(f):
)
if user is not None:
login_user(user)
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
return f(*args, **kwargs)
log.debug("Received Kobo request without a recognizable auth token.")
return abort(401)
log.debug("Received Kobo request without a recognizable auth token.")
return abort(401)
return inner
+9 -1
View File
@@ -18,9 +18,14 @@
import sys
from . import create_app
from . import create_app, limiter
from .jinjia import jinjia
from .remotelogin import remotelogin
from flask import request
def request_username():
return request.authorization.username
def main():
app = create_app()
@@ -39,6 +44,7 @@ def main():
try:
from .kobo import kobo, get_kobo_activated
from .kobo_auth import kobo_auth
from flask_limiter.util import get_remote_address
kobo_available = get_kobo_activated()
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
kobo_available = False
@@ -56,6 +62,7 @@ def main():
app.register_blueprint(tasks)
app.register_blueprint(web)
app.register_blueprint(opds)
limiter.limit("3/minute",key_func=request_username)(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(shelf)
@@ -67,6 +74,7 @@ def main():
if kobo_available:
app.register_blueprint(kobo)
app.register_blueprint(kobo_auth)
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
if oauth_available:
app.register_blueprint(oauth)
success = web_server.start()
+5 -5
View File
@@ -63,11 +63,11 @@ class Amazon(Metadata):
r.raise_for_status()
except Exception as ex:
log.warning(ex)
return
return None
long_soup = BS(r.text, "lxml") #~4sec :/
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"})
if soup2 is None:
return
return None
try:
match = MetaRecord(
title = "",
@@ -115,7 +115,7 @@ class Amazon(Metadata):
return match, index
except Exception as e:
log.error_or_exception(e)
return
return None
val = list()
if self.active:
@@ -127,10 +127,10 @@ class Amazon(Metadata):
results.raise_for_status()
except requests.exceptions.HTTPError as e:
log.error_or_exception(e)
return None
return []
except Exception as e:
log.warning(e)
return None
return []
soup = BS(results.text, 'html.parser')
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
+100 -48
View File
@@ -43,7 +43,8 @@ class Douban(Metadata):
__id__ = "douban"
DESCRIPTION = "豆瓣"
META_URL = "https://book.douban.com/"
SEARCH_URL = "https://www.douban.com/j/search"
SEARCH_JSON_URL = "https://www.douban.com/j/search"
SEARCH_URL = "https://www.douban.com/search"
ID_PATTERN = re.compile(r"sid: (?P<id>\d+),")
AUTHORS_PATTERN = re.compile(r"作者|译者")
@@ -52,6 +53,7 @@ class Douban(Metadata):
PUBLISHED_DATE_PATTERN = re.compile(r"出版年")
SERIES_PATTERN = re.compile(r"丛书")
IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号")
CRITERIA_PATTERN = re.compile("criteria = '(.+)'")
TITTLE_XPATH = "//span[@property='v:itemreviewed']"
COVER_XPATH = "//a[@class='nbg']"
@@ -63,56 +65,90 @@ class Douban(Metadata):
session = requests.Session()
session.headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
}
def search(
self, query: str, generic_cover: str = "", locale: str = "en"
) -> Optional[List[MetaRecord]]:
def search(self,
query: str,
generic_cover: str = "",
locale: str = "en") -> List[MetaRecord]:
val = []
if self.active:
log.debug(f"starting search {query} on douban")
log.debug(f"start searching {query} on douban")
if title_tokens := list(
self.get_title_tokens(query, strip_joiners=False)
):
self.get_title_tokens(query, strip_joiners=False)):
query = "+".join(title_tokens)
try:
r = self.session.get(
self.SEARCH_URL, params={"cat": 1001, "q": query}
)
r.raise_for_status()
book_id_list = self._get_book_id_list_from_html(query)
except Exception as e:
log.warning(e)
return None
results = r.json()
if results["total"] == 0:
if not book_id_list:
log.debug("No search results in Douban")
return []
book_id_list = [
self.ID_PATTERN.search(item).group("id")
for item in results["items"][:10] if self.ID_PATTERN.search(item)
]
with futures.ThreadPoolExecutor(max_workers=5) as executor:
with futures.ThreadPoolExecutor(
max_workers=5, thread_name_prefix='douban') as executor:
fut = [
executor.submit(self._parse_single_book, book_id, generic_cover)
for book_id in book_id_list
executor.submit(self._parse_single_book, book_id,
generic_cover) for book_id in book_id_list
]
val = [
future.result()
for future in futures.as_completed(fut) if future.result()
future.result() for future in futures.as_completed(fut)
if future.result()
]
return val
def _parse_single_book(
self, id: str, generic_cover: str = ""
) -> Optional[MetaRecord]:
def _get_book_id_list_from_html(self, query: str) -> List[str]:
try:
r = self.session.get(self.SEARCH_URL,
params={
"cat": 1001,
"q": query
})
r.raise_for_status()
except Exception as e:
log.warning(e)
return []
html = etree.HTML(r.content.decode("utf8"))
result_list = html.xpath(self.COVER_XPATH)
return [
self.ID_PATTERN.search(item.get("onclick")).group("id")
for item in result_list[:10]
if self.ID_PATTERN.search(item.get("onclick"))
]
def _get_book_id_list_from_json(self, query: str) -> List[str]:
try:
r = self.session.get(self.SEARCH_JSON_URL,
params={
"cat": 1001,
"q": query
})
r.raise_for_status()
except Exception as e:
log.warning(e)
return []
results = r.json()
if results["total"] == 0:
return []
return [
self.ID_PATTERN.search(item).group("id")
for item in results["items"][:10] if self.ID_PATTERN.search(item)
]
def _parse_single_book(self,
id: str,
generic_cover: str = "") -> Optional[MetaRecord]:
url = f"https://book.douban.com/subject/{id}/"
log.debug(f"start parsing {url}")
try:
r = self.session.get(url)
@@ -136,7 +172,8 @@ class Douban(Metadata):
html = etree.HTML(r.content.decode("utf8"))
match.title = html.xpath(self.TITTLE_XPATH)[0].text
match.cover = html.xpath(self.COVER_XPATH)[0].attrib["href"] or generic_cover
match.cover = html.xpath(
self.COVER_XPATH)[0].attrib["href"] or generic_cover
try:
rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip())
except Exception:
@@ -146,35 +183,39 @@ class Douban(Metadata):
tag_elements = html.xpath(self.TAGS_XPATH)
if len(tag_elements):
match.tags = [tag_element.text for tag_element in tag_elements]
else:
match.tags = self._get_tags(html.text)
description_element = html.xpath(self.DESCRIPTION_XPATH)
if len(description_element):
match.description = html2text(etree.tostring(
description_element[-1], encoding="utf8").decode("utf8"))
match.description = html2text(
etree.tostring(description_element[-1]).decode("utf8"))
info = html.xpath(self.INFO_XPATH)
for element in info:
text = element.text
if self.AUTHORS_PATTERN.search(text):
next = element.getnext()
while next is not None and next.tag != "br":
match.authors.append(next.text)
next = next.getnext()
next_element = element.getnext()
while next_element is not None and next_element.tag != "br":
match.authors.append(next_element.text)
next_element = next_element.getnext()
elif self.PUBLISHER_PATTERN.search(text):
match.publisher = element.tail.strip()
if publisher := element.tail.strip():
match.publisher = publisher
else:
match.publisher = element.getnext().text
elif self.SUBTITLE_PATTERN.search(text):
match.title = f'{match.title}:' + element.tail.strip()
match.title = f'{match.title}:{element.tail.strip()}'
elif self.PUBLISHED_DATE_PATTERN.search(text):
match.publishedDate = self._clean_date(element.tail.strip())
elif self.SUBTITLE_PATTERN.search(text):
elif self.SERIES_PATTERN.search(text):
match.series = element.getnext().text
elif i_type := self.IDENTIFIERS_PATTERN.search(text):
match.identifiers[i_type.group()] = element.tail.strip()
return match
def _clean_date(self, date: str) -> str:
"""
Clean up the date string to be in the format YYYY-MM-DD
@@ -194,13 +235,24 @@ class Douban(Metadata):
if date[i].isdigit():
digit.append(date[i])
elif digit:
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}")
ls.append("".join(digit) if len(digit) ==
2 else f"0{digit[0]}")
digit = []
if digit:
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}")
ls.append("".join(digit) if len(digit) ==
2 else f"0{digit[0]}")
moon = ls[0]
if len(ls)>1:
day = ls[1]
if len(ls) > 1:
day = ls[1]
return f"{year}-{moon}-{day}"
def _get_tags(self, text: str) -> List[str]:
tags = []
if criteria := self.CRITERIA_PATTERN.search(text):
tags.extend(
item.replace('7:', '') for item in criteria.group().split('|')
if item.startswith('7:'))
return tags
+13 -1
View File
@@ -19,6 +19,7 @@
# Google Books api document: https://developers.google.com/books/docs/v1/using
from typing import Dict, List, Optional
from urllib.parse import quote
from datetime import datetime
import requests
@@ -81,7 +82,11 @@ class Google(Metadata):
match.description = result["volumeInfo"].get("description", "")
match.languages = self._parse_languages(result=result, locale=locale)
match.publisher = result["volumeInfo"].get("publisher", "")
match.publishedDate = result["volumeInfo"].get("publishedDate", "")
try:
datetime.strptime(result["volumeInfo"].get("publishedDate", ""), "%Y-%m-%d")
match.publishedDate = result["volumeInfo"].get("publishedDate", "")
except ValueError:
match.publishedDate = ""
match.rating = result["volumeInfo"].get("averageRating", 0)
match.series, match.series_index = "", 1
match.tags = result["volumeInfo"].get("categories", [])
@@ -103,6 +108,13 @@ class Google(Metadata):
def _parse_cover(result: Dict, generic_cover: str) -> str:
if result["volumeInfo"].get("imageLinks"):
cover_url = result["volumeInfo"]["imageLinks"]["thumbnail"]
# strip curl in cover
cover_url = cover_url.replace("&edge=curl", "")
# request 800x900 cover image (higher resolution)
cover_url += "&fife=w800-h900"
return cover_url.replace("http://", "https://")
return generic_cover
+1 -1
View File
@@ -54,7 +54,7 @@ class scholar(Metadata):
scholar_gen = itertools.islice(scholarly.search_pubs(query), 10)
except Exception as e:
log.warning(e)
return None
return list()
for result in scholar_gen:
match = self._parse_search_result(
result=result, generic_cover="", locale=locale
+21 -21
View File
@@ -74,7 +74,7 @@ def register_user_with_oauth(user=None):
if len(all_oauth.keys()) == 0:
return
if user is None:
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
flash(_("Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
else:
for oauth_key in all_oauth.keys():
# Find this OAuth token in the database, or create it
@@ -134,8 +134,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
# already bind with user, just login
if oauth_entry.user:
login_user(oauth_entry.user)
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name),
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
flash(_("Success! You are now logged in as: %(nickname)s", nickname= oauth_entry.user.name),
category="success")
return redirect(url_for('web.index'))
else:
@@ -145,21 +145,21 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
try:
ub.session.add(oauth_entry)
ub.session.commit()
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
flash(_("Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
log.info("Link to {} Succeeded".format(provider_name))
return redirect(url_for('web.profile'))
except Exception as ex:
log.error_or_exception(ex)
ub.session.rollback()
else:
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
flash(_("Login failed, No User Linked With OAuth Account"), category="error")
log.info('Login failed, No User Linked With OAuth Account')
return redirect(url_for('web.login'))
# return redirect(url_for('web.login'))
# if config.config_public_reg:
# return redirect(url_for('web.register'))
# else:
# flash(_(u"Public registration is not enabled"), category="error")
# flash(_("Public registration is not enabled"), category="error")
# return redirect(url_for(redirect_url))
except (NoResultFound, AttributeError):
return redirect(url_for(redirect_url))
@@ -194,15 +194,15 @@ def unlink_oauth(provider):
ub.session.delete(oauth_entry)
ub.session.commit()
logout_oauth_user()
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
flash(_("Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
except Exception as ex:
log.error_or_exception(ex)
ub.session.rollback()
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
flash(_("Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound:
log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
flash(_("Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile'))
def generate_oauth_blueprints():
@@ -258,13 +258,13 @@ if ub.oauth_support:
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
def github_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with GitHub."), category="error")
flash(_("Failed to log in with GitHub."), category="error")
log.error("Failed to log in with GitHub")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error")
flash(_("Failed to fetch user info from GitHub."), category="error")
log.error("Failed to fetch user info from GitHub")
return False
@@ -276,13 +276,13 @@ if ub.oauth_support:
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
def google_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with Google."), category="error")
flash(_("Failed to log in with Google."), category="error")
log.error("Failed to log in with Google")
return False
resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error")
flash(_("Failed to fetch user info from Google."), category="error")
log.error("Failed to fetch user info from Google")
return False
@@ -295,8 +295,8 @@ if ub.oauth_support:
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
def github_error(blueprint, error, error_description=None, error_uri=None):
msg = (
u"OAuth error from {name}! "
u"error={error} description={description} uri={uri}"
"OAuth error from {name}! "
"error={error} description={description} uri={uri}"
).format(
name=blueprint.name,
error=error,
@@ -308,8 +308,8 @@ if ub.oauth_support:
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
def google_error(blueprint, error, error_description=None, error_uri=None):
msg = (
u"OAuth error from {name}! "
u"error={error} description={description} uri={uri}"
"OAuth error from {name}! "
"error={error} description={description} uri={uri}"
).format(
name=blueprint.name,
error=error,
@@ -329,10 +329,10 @@ def github_login():
if account_info.ok:
account_info_json = account_info.json()
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
flash(_(u"GitHub Oauth error, please retry later."), category="error")
flash(_("GitHub Oauth error, please retry later."), category="error")
log.error("GitHub Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e:
flash(_(u"GitHub Oauth error: {}").format(e), category="error")
flash(_("GitHub Oauth error: {}").format(e), category="error")
log.error(e)
return redirect(url_for('web.login'))
@@ -353,10 +353,10 @@ def google_login():
if resp.ok:
account_info_json = resp.json()
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
flash(_(u"Google Oauth error, please retry later."), category="error")
flash(_("Google Oauth error, please retry later."), category="error")
log.error("Google Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e:
flash(_(u"Google Oauth error: {}").format(e), category="error")
flash(_("Google Oauth error: {}").format(e), category="error")
log.error(e)
return redirect(url_for('web.login'))
+9 -47
View File
@@ -22,40 +22,26 @@
import datetime
from urllib.parse import unquote_plus
from functools import wraps
from flask import Blueprint, request, render_template, Response, g, make_response, abort
from flask import Blueprint, request, render_template, make_response, abort
from flask_login import current_user
from flask_babel import get_locale
from flask_babel import gettext as _
from sqlalchemy.sql.expression import func, text, or_, and_, true
from sqlalchemy.exc import InvalidRequestError, OperationalError
from werkzeug.security import check_password_hash
from . import constants, logger, config, db, calibre_db, ub, services, isoLanguages
from . import logger, config, db, calibre_db, ub, isoLanguages
from .usermanagement import requires_basic_auth_if_no_ano
from .helper import get_download_link, get_book_cover
from .pagination import Pagination
from .web import render_read_books
from .usermanagement import load_user_from_request
from flask_babel import gettext as _
opds = Blueprint('opds', __name__)
log = logger.create()
def requires_basic_auth_if_no_ano(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if config.config_anonbrowse != 1:
if not auth or auth.type != 'basic' or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and config.config_anonbrowse != 1:
return services.ldap.basic_auth_required(f)
return decorated
@opds.route("/opds/")
@opds.route("/opds")
@requires_basic_auth_if_no_ano
@@ -328,7 +314,7 @@ def feed_format(book_id):
@requires_basic_auth_if_no_ano
def feed_languagesindex():
off = request.args.get("offset") or 0
if current_user.filter_language() == u"all":
if current_user.filter_language() == "all":
languages = calibre_db.speaking_language()
else:
languages = calibre_db.session.query(db.Languages).filter(
@@ -355,7 +341,8 @@ def feed_languages(book_id):
@requires_basic_auth_if_no_ano
def feed_shelfindex():
off = request.args.get("offset") or 0
shelf = g.shelves_access
shelf = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
number = len(shelf)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
number)
@@ -402,11 +389,7 @@ def feed_shelf(book_id):
@opds.route("/opds/download/<book_id>/<book_format>/")
@requires_basic_auth_if_no_ano
def opds_download_link(book_id, book_format):
# I gave up with this: With enabled ldap login, the user doesn't get logged in, therefore it's always guest
# workaround, loading the user from the request and checking its download rights here
# in case of anonymous browsing user is None
user = load_user_from_request(request) or current_user
if not user.role_download():
if not current_user.role_download():
return abort(403)
if "Kobo" in request.headers.get('User-Agent'):
client = "kobo"
@@ -478,27 +461,6 @@ def feed_search(term):
return render_xml_template('feed.xml', searchterm="")
def check_auth(username, password):
try:
username = username.encode('windows-1252')
except UnicodeEncodeError:
username = username.encode('utf-8')
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
username.decode('utf-8').lower()).first()
if bool(user and check_password_hash(str(user.password), password)):
return True
else:
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_address)
return False
def authenticate():
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def render_xml_template(*args, **kwargs):
# ToDo: return time in current timezone similar to %z
+12 -12
View File
@@ -58,8 +58,8 @@ def remote_login():
ub.session.add(auth_token)
ub.session_commit()
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
return render_title_template('remote_login.html', title=_(u"Login"), token=auth_token.auth_token,
log.debug("Remot Login request with token: %s", auth_token.auth_token)
return render_title_template('remote_login.html', title=_("Login"), token=auth_token.auth_token,
verify_url=verify_url, page="remotelogin")
@@ -71,8 +71,8 @@ def verify_token(token):
# Token not found
if auth_token is None:
flash(_(u"Token not found"), category="error")
log.error(u"Remote Login token not found")
flash(_("Token not found"), category="error")
log.error("Remote Login token not found")
return redirect(url_for('web.index'))
# Token expired
@@ -80,8 +80,8 @@ def verify_token(token):
ub.session.delete(auth_token)
ub.session_commit()
flash(_(u"Token has expired"), category="error")
log.error(u"Remote Login token expired")
flash(_("Token has expired"), category="error")
log.error("Remote Login token expired")
return redirect(url_for('web.index'))
# Update token with user information
@@ -89,8 +89,8 @@ def verify_token(token):
auth_token.verified = True
ub.session_commit()
flash(_(u"Success! Please return to your device"), category="success")
log.debug(u"Remote Login token for userid %s verified", auth_token.user_id)
flash(_("Success! Please return to your device"), category="success")
log.debug("Remote Login token for userid %s verified", auth_token.user_id)
return redirect(url_for('web.index'))
@@ -105,7 +105,7 @@ def token_verified():
# Token not found
if auth_token is None:
data['status'] = 'error'
data['message'] = _(u"Token not found")
data['message'] = _("Token not found")
# Token expired
elif datetime.now() > auth_token.expiration:
@@ -113,7 +113,7 @@ def token_verified():
ub.session_commit()
data['status'] = 'error'
data['message'] = _(u"Token has expired")
data['message'] = _("Token has expired")
elif not auth_token.verified:
data['status'] = 'not_verified'
@@ -126,8 +126,8 @@ def token_verified():
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
data['status'] = 'success'
log.debug(u"Remote Login for userid %s succeeded", user.id)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
log.debug("Remote Login for userid %s succeeded", user.id)
flash(_("Success! You are now logged in as: %(nickname)s", nickname=user.name), category="success")
response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8"
+22 -17
View File
@@ -20,11 +20,13 @@ from flask import render_template, g, abort, request
from flask_babel import gettext as _
from werkzeug.local import LocalProxy
from flask_login import current_user
from sqlalchemy.sql.expression import or_
from . import config, constants, logger
from . import config, constants, logger, ub
from .ub import User
log = logger.create()
def get_sidebar_config(kwargs=None):
@@ -45,12 +47,12 @@ def get_sidebar_config(kwargs=None):
"show_text": _('Show Hot Books'), "config_show": True})
if current_user.role_admin():
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
else:
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
sidebar.append(
@@ -58,47 +60,50 @@ def get_sidebar_config(kwargs=None):
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
"show_text": _('Show Top Rated Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
"page": "read", "show_text": _('Show read and unread'), "config_show": content})
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous),
"page": "read", "show_text": _('Show Read and Unread'), "config_show": content})
sidebar.append(
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous), "page": "unread",
"show_text": _('Show unread'), "config_show": False})
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
"show_text": _('Show Random Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
"show_text": _('Show category selection'), "config_show": True})
"show_text": _('Show Category Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
"show_text": _('Show series selection'), "config_show": True})
"show_text": _('Show Series Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
"show_text": _('Show author selection'), "config_show": True})
"show_text": _('Show Author Section'), "config_show": True})
sidebar.append(
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
"show_text": _('Show publisher selection'), "config_show":True})
"show_text": _('Show Publisher Section'), "config_show":True})
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'),
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'),
"page": "language",
"show_text": _('Show language selection'), "config_show": True})
"show_text": _('Show Language Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
"visibility": constants.SIDEBAR_RATING, 'public': True,
"page": "rating", "show_text": _('Show ratings selection'), "config_show": True})
"page": "rating", "show_text": _('Show Ratings Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
"visibility": constants.SIDEBAR_FORMAT, 'public': True,
"page": "format", "show_text": _('Show file formats selection'), "config_show": True})
"page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
sidebar.append(
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
"show_text": _('Show archived books'), "config_show": content})
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not current_user.is_anonymous), "page": "archived",
"show_text": _('Show Archived Books'), "config_show": content})
if not simple:
sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous), "page": "list",
"show_text": _('Show Books List'), "config_show": content})
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
return sidebar, simple
+11 -7
View File
@@ -19,19 +19,22 @@
import datetime
from . import config, constants
from .services.background_scheduler import BackgroundScheduler, use_APScheduler
from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
from .tasks.database import TaskReconnectDatabase
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
from .services.worker import WorkerThread
from .tasks.metadata_backup import TaskBackupMetadata
def get_scheduled_tasks(reconnect=True):
tasks = list()
# config.schedule_reconnect or
# Reconnect Calibre database (metadata.db)
# Reconnect Calibre database (metadata.db) based on config.schedule_reconnect
if reconnect:
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
# Generate metadata.opf file for each changed book
if config.schedule_metadata_backup:
tasks.append([lambda: TaskBackupMetadata("en"), 'backup metadata', False])
# Generate all missing book cover thumbnails
if config.schedule_generate_book_covers:
tasks.append([lambda: TaskClearCoverThumbnailCache(0), 'delete superfluous book covers', True])
@@ -62,10 +65,10 @@ def register_scheduled_tasks(reconnect=True):
duration = config.schedule_duration
# Register scheduled tasks
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger='cron', hour=start)
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger=CronTrigger(hour=start))
end_time = calclulate_end_time(start, duration)
scheduler.schedule(func=end_scheduled_tasks, trigger='cron', name="end scheduled task", hour=end_time.hour,
minute=end_time.minute)
scheduler.schedule(func=end_scheduled_tasks, trigger=CronTrigger(hour=end_time.hour, minute=end_time.minute),
name="end scheduled task")
# Kick-off tasks, if they should currently be running
if should_task_be_running(start, duration):
@@ -91,6 +94,7 @@ def should_task_be_running(start, duration):
end_time = start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
return start_time < now < end_time
def calclulate_end_time(start, duration):
start_time = datetime.datetime.now().replace(hour=start, minute=0)
return start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
+19 -18
View File
@@ -35,17 +35,18 @@ search = Blueprint('search', __name__)
log = logger.create()
@search.route("/search", methods=["GET"])
@search.route("/search", methods=["POST"])
@login_required_if_no_ano
def simple_search():
term = request.args.get("query")
term = dict(request.form).get("query")
if term:
return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term.strip()))
flask_session['query'] = json.dumps(term.strip())
return redirect(url_for('web.books_list', data="search", sort_param='stored', query="")) # term.strip()
else:
return render_title_template('search.html',
searchterm="",
result_count=0,
title=_(u"Search"),
title=_("Search"),
page="search")
@@ -185,18 +186,18 @@ def extend_search_term(searchterm,
searchterm.extend((author_name.replace('|', ','), book_title, publisher))
if pub_start:
try:
searchterm.extend([_(u"Published after ") +
searchterm.extend([_("Published after ") +
format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
format='medium')])
except ValueError:
pub_start = u""
pub_start = ""
if pub_end:
try:
searchterm.extend([_(u"Published before ") +
searchterm.extend([_("Published before ") +
format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
format='medium')])
except ValueError:
pub_end = u""
pub_end = ""
elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
for key, db_element in elements.items():
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
@@ -214,11 +215,11 @@ def extend_search_term(searchterm,
language_names = calibre_db.speaking_language(language_names)
searchterm.extend(language.name for language in language_names)
if rating_high:
searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
searchterm.extend([_("Rating <= %(rating)s", rating=rating_high)])
if rating_low:
searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
searchterm.extend([_("Rating >= %(rating)s", rating=rating_low)])
if read_status:
searchterm.extend([_(u"Read Status = %(status)s", status=read_status)])
searchterm.extend([_("Read Status = %(status)s", status=read_status)])
searchterm.extend(ext for ext in tags['include_extension'])
searchterm.extend(ext for ext in tags['exclude_extension'])
# handle custom columns
@@ -267,19 +268,19 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
column_start = term.get('custom_column_' + str(c.id) + '_start')
column_end = term.get('custom_column_' + str(c.id) + '_end')
if column_start:
search_term.extend([u"{} >= {}".format(c.name,
search_term.extend(["{} >= {}".format(c.name,
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
format='medium')
)])
cc_present = True
if column_end:
search_term.extend([u"{} <= {}".format(c.name,
search_term.extend(["{} <= {}".format(c.name,
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
format='medium')
)])
cc_present = True
elif term.get('custom_column_' + str(c.id)):
search_term.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
cc_present = True
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
@@ -339,7 +340,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
pagination=pagination,
entries=entries,
result_count=result_count,
title=_(u"Advanced Search"), page="advsearch",
title=_("Advanced Search"), page="advsearch",
order=order[1])
@@ -366,12 +367,12 @@ def render_prepare_search_form(cc):
.filter(calibre_db.common_filters()) \
.group_by(db.Data.format)\
.order_by(db.Data.format).all()
if current_user.filter_language() == u"all":
if current_user.filter_language() == "all":
languages = calibre_db.speaking_language()
else:
languages = None
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
series=series,shelves=shelves, title=_(u"Advanced Search"), cc=cc, page="advsearch")
series=series,shelves=shelves, title=_("Advanced Search"), cc=cc, page="advsearch")
def render_search_results(term, offset=None, order=None, limit=None):
@@ -389,7 +390,7 @@ def render_search_results(term, offset=None, order=None, limit=None):
adv_searchterm=term,
entries=entries,
result_count=result_count,
title=_(u"Search"),
title=_("Search"),
page="search",
order=order[1])
+12 -2
View File
@@ -152,7 +152,7 @@ class WebServer(object):
# The value of __package__ indicates how Python was called. It may
# not exist if a setuptools script is installed as an egg. It may be
# set incorrectly for entry points created with pip on Windows.
if getattr(__main__, "__package__", None) is None or (
if getattr(__main__, "__package__", "") in ["", None] or (
os.name == "nt"
and __main__.__package__ == ""
and not os.path.exists(py_script)
@@ -193,6 +193,8 @@ class WebServer(object):
rv.extend(("-m", py_module.lstrip(".")))
rv.extend(args)
if os.name == 'nt':
rv = ['"{}"'.format(a) for a in rv]
return rv
def _start_gevent(self):
@@ -262,9 +264,16 @@ class WebServer(object):
log.info("Performing restart of Calibre-Web")
args = self._get_args_for_reloading()
subprocess.call(args, close_fds=True) # nosec
os.execv(args[0].lstrip('"').rstrip('"'), args)
return True
@staticmethod
def shutdown_scheduler():
from .services.background_scheduler import BackgroundScheduler
scheduler = BackgroundScheduler()
if scheduler:
scheduler.scheduler.shutdown()
def _killServer(self, __, ___):
self.stop()
@@ -273,6 +282,7 @@ class WebServer(object):
updater_thread.stop()
log.info("webserver stop (restart=%s)", restart)
self.shutdown_scheduler()
self.restart = restart
if self.wsgiserver:
if _GEVENT:
+9 -9
View File
@@ -23,6 +23,8 @@ from .worker import WorkerThread
try:
from apscheduler.schedulers.background import BackgroundScheduler as BScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
use_APScheduler = True
except (ImportError, RuntimeError) as e:
use_APScheduler = False
@@ -43,35 +45,33 @@ class BackgroundScheduler:
cls.scheduler = BScheduler()
cls.scheduler.start()
atexit.register(lambda: cls.scheduler.shutdown())
return cls._instance
def schedule(self, func, trigger, name=None, **trigger_args):
def schedule(self, func, trigger, name=None):
if use_APScheduler:
return self.scheduler.add_job(func=func, trigger=trigger, name=name, **trigger_args)
return self.scheduler.add_job(func=func, trigger=trigger, name=name)
# Expects a lambda expression for the task
def schedule_task(self, task, user=None, name=None, hidden=False, trigger='cron', **trigger_args):
def schedule_task(self, task, user=None, name=None, hidden=False, trigger=None):
if use_APScheduler:
def scheduled_task():
worker_task = task()
worker_task.scheduled = True
WorkerThread.add(user, worker_task, hidden=hidden)
return self.schedule(func=scheduled_task, trigger=trigger, name=name, **trigger_args)
return self.schedule(func=scheduled_task, trigger=trigger, name=name)
# Expects a list of lambda expressions for the tasks
def schedule_tasks(self, tasks, user=None, trigger='cron', **trigger_args):
def schedule_tasks(self, tasks, user=None, trigger=None):
if use_APScheduler:
for task in tasks:
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2], **trigger_args)
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2])
# Expects a lambda expression for the task
def schedule_task_immediately(self, task, user=None, name=None, hidden=False):
if use_APScheduler:
def immediate_task():
WorkerThread.add(user, task(), hidden)
return self.schedule(func=immediate_task, trigger='date', name=name)
return self.schedule(func=immediate_task, trigger=DateTrigger(), name=name)
# Expects a list of lambda expressions for the tasks
def schedule_tasks_immediately(self, tasks, user=None):
+47 -7
View File
@@ -20,6 +20,7 @@ import base64
from flask_simpleldap import LDAP, LDAPException
from flask_simpleldap import ldap as pyLDAP
from flask import current_app
from .. import constants, logger
try:
@@ -28,8 +29,47 @@ except ImportError:
pass
log = logger.create()
_ldap = LDAP()
class LDAPLogger(object):
def write(self, message):
try:
log.debug(message.strip("\n").replace("\n", ""))
except Exception:
log.debug("Logging Error")
class mySimpleLDap(LDAP):
@staticmethod
def init_app(app):
super(mySimpleLDap, mySimpleLDap).init_app(app)
app.config.setdefault('LDAP_LOGLEVEL', 0)
@property
def initialize(self):
"""Initialize a connection to the LDAP server.
:return: LDAP connection object.
"""
try:
log_level = 2 if current_app.config['LDAP_LOGLEVEL'] == logger.logging.DEBUG else 0
conn = pyLDAP.initialize('{0}://{1}:{2}'.format(
current_app.config['LDAP_SCHEMA'],
current_app.config['LDAP_HOST'],
current_app.config['LDAP_PORT']), trace_level=log_level, trace_file=LDAPLogger())
conn.set_option(pyLDAP.OPT_NETWORK_TIMEOUT,
current_app.config['LDAP_TIMEOUT'])
conn = self._set_custom_options(conn)
conn.protocol_version = pyLDAP.VERSION3
if current_app.config['LDAP_USE_TLS']:
conn.start_tls_s()
return conn
except pyLDAP.LDAPError as e:
raise LDAPException(self.error(e.args))
_ldap = mySimpleLDap()
def init_app(app, config):
if config.config_login_type != constants.LOGIN_LDAP:
@@ -44,15 +84,15 @@ def init_app(app, config):
app.config['LDAP_SCHEMA'] = 'ldap'
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if config.config_ldap_serv_password is None:
config.config_ldap_serv_password = ''
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password)
if config.config_ldap_serv_password_e is None:
config.config_ldap_serv_password_e = ''
app.config['LDAP_PASSWORD'] = config.config_ldap_serv_password_e
else:
app.config['LDAP_PASSWORD'] = base64.b64decode("")
app.config['LDAP_PASSWORD'] = ""
app.config['LDAP_USERNAME'] = config.config_ldap_serv_username
else:
app.config['LDAP_USERNAME'] = ""
app.config['LDAP_PASSWORD'] = base64.b64decode("")
app.config['LDAP_PASSWORD'] = ""
if bool(config.config_ldap_cert_path):
app.config['LDAP_CUSTOM_OPTIONS'].update({
pyLDAP.OPT_X_TLS_REQUIRE_CERT: pyLDAP.OPT_X_TLS_DEMAND,
@@ -70,7 +110,7 @@ def init_app(app, config):
app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap)
app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
app.config['LDAP_LOGLEVEL'] = config.config_log_level
try:
_ldap.init_app(app)
except ValueError:
+31 -31
View File
@@ -46,13 +46,13 @@ def add_to_shelf(shelf_id, book_id):
if shelf is None:
log.error("Invalid shelf specified: %s", shelf_id)
if not xhr:
flash(_(u"Invalid shelf specified"), category="error")
flash(_("Invalid shelf specified"), category="error")
return redirect(url_for('web.index'))
return "Invalid shelf specified", 400
if not check_shelf_edit_permissions(shelf):
if not xhr:
flash(_(u"Sorry you are not allowed to add a book to that shelf"), category="error")
flash(_("Sorry you are not allowed to add a book to that shelf"), category="error")
return redirect(url_for('web.index'))
return "Sorry you are not allowed to add a book to the that shelf", 403
@@ -61,7 +61,7 @@ def add_to_shelf(shelf_id, book_id):
if book_in_shelf:
log.error("Book %s is already part of %s", book_id, shelf)
if not xhr:
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
flash(_("Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
return redirect(url_for('web.index'))
return "Book is already part of the shelf: %s" % shelf.name, 400
@@ -79,14 +79,14 @@ def add_to_shelf(shelf_id, book_id):
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
return redirect(url_for('web.index'))
if not xhr:
log.debug("Book has been added to shelf: {}".format(shelf.name))
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
flash(_("Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
@@ -100,12 +100,12 @@ def search_to_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if shelf is None:
log.error("Invalid shelf specified: {}".format(shelf_id))
flash(_(u"Invalid shelf specified"), category="error")
flash(_("Invalid shelf specified"), category="error")
return redirect(url_for('web.index'))
if not check_shelf_edit_permissions(shelf):
log.warning("You are not allowed to add a book to the shelf".format(shelf.name))
flash(_(u"You are not allowed to add a book to the shelf"), category="error")
flash(_("You are not allowed to add a book to the shelf"), category="error")
return redirect(url_for('web.index'))
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
@@ -123,7 +123,7 @@ def search_to_shelf(shelf_id):
if not books_for_shelf:
log.error("Books are already part of {}".format(shelf.name))
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
flash(_("Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
@@ -135,14 +135,14 @@ def search_to_shelf(shelf_id):
try:
ub.session.merge(shelf)
ub.session.commit()
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
flash(_("Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
else:
log.error("Could not add books to shelf: {}".format(shelf.name))
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
flash(_("Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
return redirect(url_for('web.index'))
@@ -182,13 +182,13 @@ def remove_from_shelf(shelf_id, book_id):
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
return redirect(url_for('web.index'))
if not xhr:
flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
flash(_("Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
@@ -197,7 +197,7 @@ def remove_from_shelf(shelf_id, book_id):
else:
if not xhr:
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
flash(_(u"Sorry you are not allowed to remove a book from this shelf"),
flash(_("Sorry you are not allowed to remove a book from this shelf"),
category="error")
return redirect(url_for('web.index'))
return "Sorry you are not allowed to remove a book from this shelf", 403
@@ -207,7 +207,7 @@ def remove_from_shelf(shelf_id, book_id):
@login_required
def create_shelf():
shelf = ub.Shelf()
return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate")
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
@@ -215,9 +215,9 @@ def create_shelf():
def edit_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if not check_shelf_edit_permissions(shelf):
flash(_(u"Sorry you are not allowed to edit this shelf"), category="error")
flash(_("Sorry you are not allowed to edit this shelf"), category="error")
return redirect(url_for('web.index'))
return create_edit_shelf(shelf, page_title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
return create_edit_shelf(shelf, page_title=_("Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
@@ -232,7 +232,7 @@ def delete_shelf(shelf_id):
except InvalidRequestError as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return redirect(url_for('web.index'))
@@ -269,7 +269,7 @@ def order_shelf(shelf_id):
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
result = list()
if shelf:
@@ -278,7 +278,7 @@ def order_shelf(shelf_id):
.add_columns(calibre_db.common_filters().label("visible")) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
return render_title_template('shelf_order.html', entries=result,
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
title=_("Change order of Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelforder")
else:
abort(404)
@@ -310,7 +310,7 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
if request.method == "POST":
to_save = request.form.to_dict()
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
flash(_(u"Sorry you are not allowed to create a public shelf"), category="error")
flash(_("Sorry you are not allowed to create a public shelf"), category="error")
return redirect(url_for('web.index'))
is_public = 1 if to_save.get("is_public") == "on" else 0
if config.config_kobo_sync:
@@ -327,24 +327,24 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
shelf.user_id = int(current_user.id)
ub.session.add(shelf)
shelf_action = "created"
flash_text = _(u"Shelf %(title)s created", title=shelf_title)
flash_text = _("Shelf %(title)s created", title=shelf_title)
else:
shelf_action = "changed"
flash_text = _(u"Shelf %(title)s changed", title=shelf_title)
flash_text = _("Shelf %(title)s changed", title=shelf_title)
try:
ub.session.commit()
log.info(u"Shelf {} {}".format(shelf_title, shelf_action))
log.info("Shelf {} {}".format(shelf_title, shelf_action))
flash(flash_text, category="success")
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
except (OperationalError, InvalidRequestError) as ex:
ub.session.rollback()
log.error_or_exception(ex)
log.error_or_exception("Settings Database error: {}".format(ex))
flash(_(u"Database error: %(error)s.", error=ex.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=ex.orig), category="error")
except Exception as ex:
ub.session.rollback()
log.error_or_exception(ex)
flash(_(u"There was an error"), category="error")
flash(_("There was an error"), category="error")
return render_title_template('shelf_edit.html',
shelf=shelf,
title=page_title,
@@ -366,7 +366,7 @@ def check_shelf_is_unique(title, is_public, shelf_id=False):
if not is_shelf_name_unique:
log.error("A public shelf with the name '{}' already exists.".format(title))
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=title),
flash(_("A public shelf with the name '%(title)s' already exists.", title=title),
category="error")
else:
is_shelf_name_unique = ub.session.query(ub.Shelf) \
@@ -377,7 +377,7 @@ def check_shelf_is_unique(title, is_public, shelf_id=False):
if not is_shelf_name_unique:
log.error("A private shelf with the name '{}' already exists.".format(title))
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=title),
flash(_("A private shelf with the name '%(title)s' already exists.", title=title),
category="error")
return is_shelf_name_unique
@@ -454,14 +454,14 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return render_title_template(page,
entries=result,
pagination=pagination,
title=_(u"Shelf: '%(name)s'", name=shelf.name),
title=_("Shelf: '%(name)s'", name=shelf.name),
shelf=shelf,
page="shelf")
else:
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
flash(_("Error opening shelf. Shelf does not exist or is not accessible"), category="error")
return redirect(url_for("web.index"))
+3 -33
View File
@@ -3290,9 +3290,11 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
-ms-transform-origin: center top;
transform-origin: center top;
border: 0;
left: 0 !important;
overflow-y: auto;
}
.dropdown-menu:not(.datepicker-dropdown):not(.profileDropli) {
left: 0 !important;
}
#add-to-shelves {
max-height: calc(100% - 120px);
overflow-y: auto;
@@ -4423,38 +4425,6 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
left: 49px;
margin-top: 5px
}
body:not(.blur) > .navbar > .container-fluid > .navbar-header:after, body:not(.blur) > .navbar > .container-fluid > .navbar-header:before {
color: hsla(0, 0%, 100%, .7);
cursor: pointer;
display: block;
font-family: plex-icons-new, serif;
font-size: 20px;
font-stretch: 100%;
font-style: normal;
font-variant-caps: normal;
font-variant-east-asian: normal;
font-variant-numeric: normal;
font-weight: 400;
height: 60px;
letter-spacing: normal;
line-height: 60px;
position: absolute
}
body:not(.blur) > .navbar > .container-fluid > .navbar-header:before {
content: "\EA30";
-webkit-font-variant-ligatures: normal;
font-variant-ligatures: normal;
left: 20px
}
body:not(.blur) > .navbar > .container-fluid > .navbar-header:after {
content: "\EA2F";
-webkit-font-variant-ligatures: normal;
font-variant-ligatures: normal;
left: 60px
}
}
body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row:first-of-type > div.col > h2:before, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > h2:first-of-type:before, body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before {
+4
View File
@@ -22,3 +22,7 @@ body.serieslist.grid-view div.container-fluid > div > div.col-sm-10::before {
padding: 0 0;
line-height: 15px;
}
input.datepicker {color: transparent}
input.datepicker:focus {color: transparent}
input.datepicker:focus + input {color: #555}
+14
View File
@@ -149,6 +149,20 @@ body {
word-wrap: break-word;
}
#mainContent > canvas {
display: block;
margin-left: auto;
margin-right: auto;
}
.long-strip > .mainImage {
margin-bottom: 4px;
}
.long-strip > .mainImage:last-child {
margin-bottom: 0px !important;
}
#titlebar {
min-height: 25px;
height: auto;
@@ -1,6 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M8 12a1 1 0 0 1-.707-.293l-5-5a1 1 0 0 1 1.414-1.414L8
9.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-5 5A1 1 0 0 1 8 12z"></path></svg>

Before

Width:  |  Height:  |  Size: 461 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M13 11a1 1 0 0 1-.707-.293L8 6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414l5-5a1 1 0 0 1 1.414 0l5 5A1 1 0 0 1 13 11z"></path></svg>

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

@@ -1,16 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
16"
fill="rgba(255,255,255,1)">
<path
d="M8 16a8 8 0 1 1 8-8 8.009 8.009 0 0 1-8 8zM8 2a6 6 0 1 0 6 6 6.006 6.006 0 0 0-6-6z">
</path>
<path
d="M8 7a1 1 0 0 0-1 1v3a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1z">
</path>
<circle
cx="8" cy="5" r="1.188">
</circle>
</svg>

Before

Width:  |  Height:  |  Size: 557 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M13 13c-.3 0-.5-.1-.7-.3L8 8.4l-4.3 4.3c-.9.9-2.3-.5-1.4-1.4l5-5c.4-.4 1-.4 1.4 0l5 5c.6.6.2 1.7-.7 1.7zm0-11H3C1.7 2 1.7 4 3 4h10c1.3 0 1.3-2 0-2z"/></svg>

Before

Width:  |  Height:  |  Size: 255 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M15 3.7V13c0 1.5-1.53 3-3 3H7.13c-.72 0-1.63-.5-2.13-1l-5-5s.84-1 .87-1c.13-.1.33-.2.53-.2.1 0 .3.1.4.2L4 10.6V2.7c0-.6.4-1 1-1s1 .4 1 1v4.6h1V1c0-.6.4-1 1-1s1 .4 1 1v6.3h1V1.7c0-.6.4-1 1-1s1 .4 1 1v5.7h1V3.7c0-.6.4-1 1-1s1 .4 1 1z"/></svg>

Before

Width:  |  Height:  |  Size: 339 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M8 10c-.3 0-.5-.1-.7-.3l-5-5c-.9-.9.5-2.3 1.4-1.4L8 7.6l4.3-4.3c.9-.9 2.3.5 1.4 1.4l-5 5c-.2.2-.4.3-.7.3zm5 2H3c-1.3 0-1.3 2 0 2h10c1.3 0 1.3-2 0-2z"/></svg>

Before

Width:  |  Height:  |  Size: 256 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M1 1a1 1 0 011 1v2.4A7 7 0 118 15a7 7 0 01-4.9-2 1 1 0 011.4-1.5 5 5 0 10-1-5.5H6a1 1 0 010 2H1a1 1 0 01-1-1V2a1 1 0 011-1z"/></svg>

Before

Width:  |  Height:  |  Size: 231 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"></path></svg>

Before

Width:  |  Height:  |  Size: 521 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"/></svg>

Before

Width:  |  Height:  |  Size: 302 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"/></svg>

Before

Width:  |  Height:  |  Size: 307 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"/></svg>

Before

Width:  |  Height:  |  Size: 509 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>

Before

Width:  |  Height:  |  Size: 505 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M6 3c-1 0-1.5.5-1.5 1.5v7c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5v-7c0-1-.5-1.5-1.5-1.5z"/></svg>

Before

Width:  |  Height:  |  Size: 196 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M10.56 3.5C9.56 3.5 9 4 9 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.93 1.2c.8 0 1.4.2 1.8.64.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.44-.2.3-.6.6-1 .93l-.6.4c-.4.3-.6.4-.7.55-.1.1-.2.2-.3.4h3.2v1.27h-5c0-.5.1-1 .3-1.43.2-.49.7-1 1.5-1.54.7-.5 1.1-.8 1.3-1.02.3-.3.4-.7.4-1.05 0-.3-.1-.6-.3-.77-.2-.2-.4-.3-.7-.3-.4 0-.7.2-.9.5-.1.2-.1.5-.2.9h-1.4c0-.6.2-1.1.3-1.5.4-.7 1.1-1.1 2-1.1zM1.54 3.5C.54 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.54 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.8 1.125H4.5V12H3V6.9H1.3v-1c.5 0 .8 0 .97-.03.33-.07.53-.17.73-.37.1-.2.2-.3.25-.5.05-.2.05-.3.05-.3z"/></svg>

Before

Width:  |  Height:  |  Size: 705 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M4 16V2s0-1 1-1h6s1 0 1 1v14l-4-5z"/></svg>

Before

Width:  |  Height:  |  Size: 142 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="m14 9h-6c-1.3 0-1.3 2 0 2h6c1.3 0 1.3-2 0-2zm-5.2-8h-3.8c-1.3 0-1.3 2 0 2h1.7zm-6.8 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.3 1.7-0.7 0-0.5-0.4-1-1-1zm3 8c-1 0-1.3 1-0.7 1.7 0.6 0.6 1.7 0.2 1.7-0.7 0-0.5-0.4-1-1-1zm0.3-4h-0.3c-1.4 0-1.4 2 0 2h2.3zm-3.3 0c-0.9 0-1.4 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.7 0-0.6-0.5-1-1-1zm12 8h-9c-1.3 0-1.3 2 0 2h9c1.3 0 1.3-2 0-2zm-12 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.712 0-0.5-0.4-1-1-1z"/><path d="m7.37 4.838 3.93-3.911v2.138h3.629v3.546h-3.629v2.138l-3.93-3.911"/></svg>

After

Width:  |  Height:  |  Size: 581 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M14 3h-2v2h2v8H2V5h7V3h-.849L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM2 3h3.219l1.072 1H2z"></path><path d="M8.146 6.146a.5.5 0 0 0 0 .707l2 2a.5.5 0 0 0 .707 0l2-2a.5.5 0 1 0-.707-.707L11 7.293V.5a.5.5 0 0 0-1 0v6.793L8.854 6.146a.5.5 0 0 0-.708 0z"></path></svg>

Before

Width:  |  Height:  |  Size: 651 B

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- copied from https://www.svgrepo.com/svg/255881/text -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<g>
<g transform="scale(0.03125)">
<path d="M405.787,43.574H8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83c0,4.512,3.657,8.17,8.17,8.17h32.681
c4.513,0,8.17-3.658,8.17-8.17v-24.511h95.319v119.83c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-128
c0-4.512-3.657-8.17-8.17-8.17H40.851c-4.513,0-8.17,3.658-8.17,8.17v24.511H16.34V59.915h381.277v103.489h-16.34v-24.511
c0-4.512-3.657-8.17-8.17-8.17h-111.66c-4.513,0-8.17,3.658-8.17,8.17v288.681c0,4.512,3.657,8.17,8.17,8.17h57.191v16.34H95.319
v-16.34h57.191c4.513,0,8.17-3.658,8.17-8.17v-128c0-4.512-3.657-8.17-8.17-8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83H87.149
c-4.513,0-8.17,3.658-8.17,8.17v32.681c0,4.512,3.657,8.17,8.17,8.17h239.66c4.513,0,8.17-3.658,8.17-8.17v-32.681
c0-4.512-3.657-8.17-8.17-8.17h-57.192v-272.34h95.319v24.511c0,4.512,3.657,8.17,8.17,8.17h32.681c4.513,0,8.17-3.658,8.17-8.17
V51.745C413.957,47.233,410.3,43.574,405.787,43.574z"/>
</g>
</g>
<g>
<g transform="scale(0.03125)">
<path d="M503.83,452.085h-24.511V59.915h24.511c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-65.362
c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h24.511v392.17h-24.511c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17
h65.362c4.513,0,8.17-3.658,8.17-8.17S508.343,452.085,503.83,452.085z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 16 16">
<g>
<g transform="scale(0.03125)">
<path d="m455.1,137.9l-32.4,32.4-81-81.1 32.4-32.4c6.6-6.6 18.1-6.6 24.7,0l56.3,56.4c6.8,6.8 6.8,17.9 0,24.7zm-270.7,271l-81-81.1 209.4-209.7 81,81.1-209.4,209.7zm-99.7-42l60.6,60.7-84.4,23.8 23.8-84.5zm399.3-282.6l-56.3-56.4c-11-11-50.7-31.8-82.4,0l-285.3,285.5c-2.5,2.5-4.3,5.5-5.2,8.9l-43,153.1c-2,7.1 0.1,14.7 5.2,20 5.2,5.3 15.6,6.2 20,5.2l153-43.1c3.4-0.9 6.4-2.7 8.9-5.2l285.1-285.5c22.7-22.7 22.7-59.7 0-82.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 804 B

@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="rgba(255,255,255,1)"><path d="M8 11a1 1 0 01-.707-.293l-2.99-2.99c-.91-.942.471-2.324 1.414-1.414L8 8.586l2.283-2.283c.943-.91 2.324.472 1.414 1.414l-2.99 2.99A1 1 0 018 11z"/></svg>

Before

Width:  |  Height:  |  Size: 251 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M14.859 3.2a1.335 1.335 0 0 1-1.217.8H13v1h1v8H2V5h8V4h-.642a1.365 1.365 0 0 1-1.325-1.11L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-1.141-1.8zM2 3h3.219l1.072 1H2zm7.854-.146L11 1.707V8.5a.5.5 0 0 0 1 0V1.707l1.146 1.146a.5.5 0 1 0 .707-.707l-2-2a.5.5 0 0 0-.707 0l-2 2a.5.5 0 0 0 .707.707z"></path></svg>

Before

Width:  |  Height:  |  Size: 686 B

@@ -1,8 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
16"
fill="rgba(255,255,255,1)"><path transform='rotate(90) translate(0, -16)'
d="M15.707 7.293l-6-6a1 1 0 0 0-1.414 1.414L12.586 7H1a1 1 0 0 0 0 2h11.586l-4.293
4.293a1 1 0 1 0 1.414 1.414l6-6a1 1 0 0 0 0-1.414z"></path></svg>

Before

Width:  |  Height:  |  Size: 517 B

@@ -1,13 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
16"
fill="rgba(255,255,255,1)">
<path
transform='rotate(90) translate(0, -16)'
d="M15 7H3.414l4.293-4.293a1 1 0 0
0-1.414-1.414l-6 6a1 1 0 0 0 0 1.414l6 6a1 1 0 0 0 1.414-1.414L3.414 9H15a1 1 0 0
0 0-2z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 517 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M.5 1H7s0-1 1-1 1 1 1 1h6.5s.5 0 .5.5-.5.5-.5.5H.5S0 2 0 1.5.5 1 .5 1zM1 3h14v7c0 2-1 2-2 2H3c-1 0-2 0-2-2zm5 1v7l6-3.5zM3.72 15.33l.53-2s0-.5.65-.35c.51.13.38.63.38.63l-.53 2s0 .5-.64.35c-.53-.13-.39-.63-.39-.63zM11.24 15.61l-.53-1.99s0-.5.38-.63c.51-.13.64.35.64.35l.53 2s0 .5-.38.63c-.5.13-.64-.35-.65-.35z"/></svg>

Before

Width:  |  Height:  |  Size: 417 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M14 5h-1V1a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v4H2a2 2 0 0 0-2 2v5h3v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-3h3V7a2 2 0 0 0-2-2zM2.5 8a.5.5 0 1 1 .5-.5.5.5 0 0 1-.5.5zm9.5 7H4v-5h8zm0-10H4V1h8zm-6.5 7h4a.5.5 0 0 0 0-1h-4a.5.5 0 1 0 0 1zm0 2h5a.5.5 0 0 0 0-1h-5a.5.5 0 1 0 0 1z"></path></svg>

Before

Width:  |  Height:  |  Size: 610 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M15.707 14.293l-4.822-4.822a6.019 6.019 0 1 0-1.414 1.414l4.822 4.822a1 1 0 0 0 1.414-1.414zM6 10a4 4 0 1 1 4-4 4 4 0 0 1-4 4z"></path></svg>

Before

Width:  |  Height:  |  Size: 472 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M8.707 7.293l-5-5a1 1 0 0 0-1.414 1.414L6.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414zm6 0l-5-5a1 1 0 0 0-1.414 1.414L12.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414z"></path></svg>

Before

Width:  |  Height:  |  Size: 549 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M3 1h10a3.008 3.008 0 0 1 3 3v8a3.009 3.009 0 0 1-3 3H3a3.005 3.005 0 0 1-3-3V4a3.013 3.013 0 0 1 3-3zm11 11V4a1 1 0 0 0-1-1H8v10h5a1 1 0 0 0 1-1zM2 12a1 1 0 0 0 1 1h4V3H3a1 1 0 0 0-1 1v8z"></path><path d="M3.5 5h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1zm0 2h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1zm1 2h1a.5.5 0 0 0 0-1h-1a.5.5 0 0 0 0 1z"></path></svg>

Before

Width:  |  Height:  |  Size: 674 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M6.2 2s.5-.5 1.06 0c.5.5 0 1 0 1l-4.6 4.61s-2.5 2.5 0 5 5 0 5 0L13.8 6.4s1.6-1.6 0-3.2-3.2 0-3.2 0L5.8 8s-.7.7 0 1.4 1.4 0 1.4 0l3.9-3.9s.6-.5 1 0c.5.5 0 1 0 1l-3.8 4s-1.8 1.8-3.5 0C3 8.7 4.8 7 4.8 7l4.7-4.9s2.7-2.6 5.3 0c2.6 2.6 0 5.3 0 5.3l-6.2 6.3s-3.5 3.5-7 0 0-7 0-7z"/></svg>

Before

Width:  |  Height:  |  Size: 380 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.233 4.233" height="16" width="16" fill="rgba(255,255,255,1)"><path d="M.15 2.992c-.198.1-.2.266-.002.365l1.604.802a.93.93 0 00.729-.001l1.602-.801c.198-.1.197-.264 0-.364l-.695-.348c-1.306.595-2.542 0-2.542 0m-.264.53l.658-.329c.6.252 1.238.244 1.754 0l.659.329-1.536.768zM.15 1.935c-.198.1-.198.265 0 .364l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363l-.694-.35c-1.14.56-2.546.001-2.546.001m-.264.53l.664-.332c.52.266 1.261.235 1.75.002l.659.33-1.537.768zM.15.877c-.198.099-.198.264 0 .363l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363L2.481.075a.926.926 0 00-.727 0zm.43.182L2.117.29l1.538.769-1.538.768z"/></svg>

Before

Width:  |  Height:  |  Size: 712 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M14 9H8c-1.3 0-1.3 2 0 2h6c1.3 0 1.3-2 0-2zm0-8H5C3.7 1 3.7 3 5 3h9c1.3 0 1.3-2 0-2zM2 1C1 1 .7 2 1.3 2.7 2 3.3 3 3 3 2c0-.5-.4-1-1-1zm3 8c-1 0-1.3 1-.7 1.7.6.6 1.7.2 1.7-.7 0-.5-.4-1-1-1zM14 5H5C3.6 5 3.6 7 5 7h9c1.3 0 1.3-2 0-2zM2 5c-.9 0-1.4 1-.7 1.7C2 7.3 3 6.9 3 6c0-.6-.5-1-1-1zM14 13H5c-1.3 0-1.3 2 0 2h9c1.3 0 1.3-2 0-2zM2 13c-1 0-1.3 1-.7 1.7.7.6 1.7.2 1.7-.712 0-.5-.4-1-1-1z"/></svg>

Before

Width:  |  Height:  |  Size: 493 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><g style="--darkreader-inline-fill:rgba(81, 82, 83, 0.8);" data-darkreader-inline-fill=""><rect x="1" y="1" width="6" height="6" rx="1" ry="1"></rect><rect x="9" y="1" width="6" height="6" rx="1" ry="1"></rect><rect x="1" y="9" width="6" height="6" rx="1" ry="1"></rect><rect x="9" y="9" width="6" height="6" rx="1" ry="1"></rect></g></svg>

Before

Width:  |  Height:  |  Size: 662 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M14 7H9V2a1 1 0 0 0-2 0v5H2a1 1 0 0 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2z"></path></svg>

Before

Width:  |  Height:  |  Size: 424 B

@@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><rect x="2" y="7" width="12" height="2" rx="1"></rect></svg>

Before

Width:  |  Height:  |  Size: 382 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M13 9L6 5v8z"/></svg>

Before

Width:  |  Height:  |  Size: 120 B

@@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M10 13l4-7H6z"/></svg>

Before

Width:  |  Height:  |  Size: 121 B

+1621 -2630
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
.fontSizeWrapper {
position: relative;
}
.slider {
position: absolute;
top: 50%;
transform: translate(0,-50%);
width: 90%;
height: 60px;
background: transparent;
border-radius: 20px;
display: flex;
align-items: center;
box-shadow: 0px 15px 40px #7E6D5766;
}
.slider label {
font-size: 20px;
font-weight: 400;
font-family: Open Sans;
padding-right: 10px;
color: white;
}
.slider input[type="range"] {
width: 80%;
height: 5px;
background: black;
border: none;
outline: none;
}
+5
View File
@@ -140,6 +140,7 @@ table .bg-dark-danger a { color: #fff; }
.container-fluid .book {
margin-top: 20px;
max-width: 180px;
display: flex;
flex-direction: column;
}
@@ -433,3 +434,7 @@ div.log {
#detailcover:-moz-full-screen { cursor:zoom-out; border: 0; }
#detailcover:-ms-fullscreen { cursor:zoom-out; border: 0; }
#detailcover:fullscreen { cursor:zoom-out; border: 0; }
.error-list {
margin-top: 5px;
}
+8 -45
View File
@@ -16,7 +16,6 @@
*/
// Move advanced search to side-menu
$("a[href*='advanced']").parent().insertAfter("#nav_new");
$("body").addClass("blur");
$("body.stat").addClass("stats");
$("body.config").addClass("admin");
$("body.uiconfig").addClass("admin");
@@ -29,8 +28,8 @@ $("body > div.container-fluid > div > div.col-sm-10 > div.filterheader").attr("s
// Back button
curHref = window.location.href.split("/");
prevHref = document.referrer.split("/");
$(".navbar-form.navbar-left")
.before('<div class="plexBack"><a href="' + encodeURI(document.referrer) + '"></a></div>');
$(".plexBack a").attr('href', encodeURI(document.referrer));
if (history.length === 1 ||
curHref[0] +
curHref[1] +
@@ -44,14 +43,9 @@ if (history.length === 1 ||
//Weird missing a after pressing back from edit.
setTimeout(function () {
if ($(".plexBack a").length < 1) {
$(".plexBack").append('<a href="' + encodeURI(document.referrer) + '"></a>');
}
$(".plexBack a").attr('href', encodeURI(document.referrer));
}, 10);
// Home button
$(".plexBack").before('<div class="home-btn"></div>');
$("a.navbar-brand").clone().appendTo(".home-btn").empty().removeClass("navbar-brand");
/////////////////////////////////
// Start of Book Details Work //
///////////////////////////////
@@ -320,19 +314,11 @@ $(document).mouseup(function (e) {
});
});
// Split path name to array and remove blanks
url = window.location.pathname
// Move create shelf
$("#nav_createshelf").prependTo(".your-shelves");
// Create drop-down for profile and move elements to it
$("#main-nav")
.prepend('<li class="dropdown"><a href="#" class="dropdown-toggle profileDrop" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span></a><ul class="dropdown-menu profileDropli"></ul></li>');
$("#top_user").parent().addClass("dropdown").appendTo(".profileDropli");
$("#nav_about").addClass("dropdown").appendTo(".profileDropli");
$("#register").parent().addClass("dropdown").appendTo(".profileDropli");
$("#logout").parent().addClass("dropdown").appendTo(".profileDropli");
// Move About link it the profile dropdown
$(".profileDropli #top_user").parent().after($("#nav_about").addClass("dropdown"))
// Remove the modals except from some areas where they are needed
bodyClass = $("body").attr("class").split(" ");
@@ -371,31 +357,6 @@ $(document).on("click", ".dropdown-toggle", function () {
});
});
// Fade out content on page unload
// delegate all clicks on "a" tag (links)
/*$(document).on("click", "a:not(.btn-toolbar a, a[href*='shelf/remove'], .identifiers a, .bookinfo , .btn-group > a, #add-to-shelves a, #book-list a, .stat.blur a )", function () {
// get the href attribute
var newUrl = $(this).attr("href");
// veryfy if the new url exists or is a hash
if (!newUrl || newUrl[0] === "#") {
// set that hash
location.hash = newUrl;
return;
}
now, fadeout the html (whole page)
$( '.blur-wrapper' ).fadeOut(250);
$(".row-fluid .col-sm-10").fadeOut(500,function () {
// when the animation is complete, set the new location
location = newUrl;
});
// prevent the default browser behavior.
return false;
});*/
// Collapse long text into read-more
$("div.comments").readmore({
collapsedHeight: 134,
@@ -458,6 +419,8 @@ if ($("body.author").length > 0) {
}
}
// Split path name to array and remove blanks
url = window.location.pathname
// Ereader Page - add class to iframe body on ereader page after it loads.
backurl = "../../book/" + url[2]
$("body.epub #title-controls")
@@ -666,7 +629,7 @@ $("#sendbtn").attr({
$("#sendbtn2").attr({
"data-toggle-two": "tooltip",
"title": $("#sendbtn2").text(), // "Send to E-Reader",
"title": $("#sendbtn2").text(), // "Send to eReader",
"data-placement": "bottom",
"data-viewport": ".btn-toolbar"
})
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -125,7 +125,7 @@ function loadArchiveFormats(formats, cb) {
_loaded_archive_formats.push(archive_format);
break;
case 'zip':
loadScript(path + 'jszip.js', checkForLoadDone);
loadScript(path + 'jszip.min.js', checkForLoadDone);
_loaded_archive_formats.push(archive_format);
break;
case 'tar':
+30 -1
View File
@@ -1,5 +1,5 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 jkrehm
* Copyright (C) 2018-2023 jkrehm, OzzieIsaacs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,6 +17,35 @@
/* global _ */
function handleResponse (data) {
$(".row-fluid.text-center").remove();
$("#flash_danger").remove();
$("#flash_success").remove();
if (!jQuery.isEmptyObject(data)) {
if($("#bookDetailsModal").is(":visible")) {
data.forEach(function (item) {
$(".modal-header").after('<div id="flash_' + item.type +
'" class="text-center alert alert-' + item.type + '">' + item.message + '</div>');
});
} else {
data.forEach(function (item) {
$(".navbar").after('<div class="row-fluid text-center">' +
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
'</div>');
});
}
}
}
$(".sendbtn-form").click(function() {
$.ajax({
method: 'post',
url: $(this).data('href'),
success: function (data) {
handleResponse(data)
}
})
});
$(function() {
$("#have_read_form").ajaxForm();
});
+273 -149
View File
@@ -62,6 +62,7 @@ var currentImage = 0;
var imageFiles = [];
var imageFilenames = [];
var totalImages = 0;
var prevScrollPosition = 0;
var settings = {
hflip: false,
@@ -70,8 +71,8 @@ var settings = {
fitMode: kthoom.Key.B,
theme: "light",
direction: 0, // 0 = Left to Right, 1 = Right to Left
nextPage: 0, // 0 = Reset to Top, 1 = Remember Position
scrollbar: 1 // 0 = Hide Scrollbar, 1 = Show Scrollbar
scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar
pageDisplay: 0 // 0 = Single Page, 1 = Long Strip
};
kthoom.saveSettings = function() {
@@ -163,6 +164,14 @@ kthoom.ImageFile = function(file) {
this.mimeType = undefined;
break;
}
// Reset mime type for special files originating from Apple devices
// This folder may contain files having image extensions (for example .jpg) but those files are not actual images
// Trying to view these files cause corrupted/empty pages in the comic reader and files should be ignored
if (this.filename.indexOf("__MACOSX") !== -1) {
this.mimeType = undefined;
}
if ( this.mimeType !== undefined) {
this.dataURI = createURLFromArray(file.fileData, this.mimeType);
}
@@ -179,7 +188,6 @@ function initProgressClick() {
}
function loadFromArrayBuffer(ab) {
var lastCompletion = 0;
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
loadArchiveFormats(['rar', 'zip', 'tar'], function() {
// Open the file as an archive
@@ -208,9 +216,13 @@ function loadFromArrayBuffer(ab) {
"</a>" +
"</li>"
);
drawCanvas();
setImage(test.dataURI, null);
// display first page if we haven't yet
if (imageFiles.length === currentImage + 1) {
updatePage(lastCompletion);
updatePage();
}
} else {
totalImages--;
@@ -225,13 +237,6 @@ function loadFromArrayBuffer(ab) {
}
function scrollTocToActive() {
// Scroll to the thumbnail in the TOC on page change
$("#tocView").stop().animate({
scrollTop: $("#tocView a.active").position().top
}, 200);
}
function updatePage() {
$(".page").text((currentImage + 1 ) + "/" + totalImages);
// Mark the current page in the TOC
@@ -243,22 +248,40 @@ function updatePage() {
// Set it to active
.addClass("active");
// Scroll to the thumbnail in the TOC on page change
$("#tocView").stop().animate({
scrollTop: $("#tocView a.active").position().top
}, 200);
}
function updatePage() {
scrollTocToActive();
scrollCurrentImageIntoView();
updateProgress();
if (imageFiles[currentImage]) {
setImage(imageFiles[currentImage].dataURI);
} else {
setImage("loading");
}
$("body").toggleClass("dark-theme", settings.theme === "dark");
$("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0);
pageDisplayUpdate();
setTheme();
kthoom.setSettings();
kthoom.saveSettings();
}
function setTheme() {
$("body").toggleClass("dark-theme", settings.theme === "dark");
$("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0);
}
function pageDisplayUpdate() {
if(settings.pageDisplay === 0) {
$(".mainImage").addClass("hide");
$(".mainImage").eq(currentImage).removeClass("hide");
$("#mainContent").removeClass("long-strip");
} else {
$(".mainImage").removeClass("hide");
$("#mainContent").addClass("long-strip");
scrollCurrentImageIntoView();
}
}
function updateProgress(loadPercentage) {
if (settings.direction === 0) {
$("#progress .bar-read")
@@ -290,100 +313,93 @@ function updateProgress(loadPercentage) {
$("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"});
}
function setImage(url) {
var canvas = $("#mainImage")[0];
var x = $("#mainImage")[0].getContext("2d");
function setImage(url, _canvas) {
var canvas = _canvas || $(".mainImage").slice(-1)[0]; // Select the last item on the array if _canvas is null
var x = canvas.getContext("2d");
$("#mainText").hide();
if (url === "loading") {
updateScale(true);
canvas.width = innerWidth - 100;
canvas.height = 200;
if (url === "error") {
x.fillStyle = "black";
x.textAlign = "center";
x.font = "24px sans-serif";
x.strokeStyle = "black";
x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100);
x.strokeStyle = (settings.theme === "dark") ? "white" : "black";
x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100);
$(".mainImage").slice(-1).addClass("error");
} else {
if (url === "error") {
updateScale(true);
canvas.width = innerWidth - 100;
canvas.height = 200;
x.fillStyle = "black";
x.textAlign = "center";
x.font = "24px sans-serif";
x.strokeStyle = "black";
x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100);
} else {
if ($("body").css("scrollHeight") / innerHeight > 1) {
$("body").css("overflowY", "scroll");
}
var img = new Image();
img.onerror = function() {
canvas.width = innerWidth - 100;
canvas.height = 300;
updateScale(true);
x.fillStyle = "black";
x.font = "50px sans-serif";
x.strokeStyle = "black";
x.fillText("Page #" + (currentImage + 1) + " (" +
imageFiles[currentImage].filename + ")", innerWidth / 2, 100);
x.fillStyle = "black";
x.fillText("Is corrupt or not an image", innerWidth / 2, 200);
var xhr = new XMLHttpRequest();
if (/(html|htm)$/.test(imageFiles[currentImage].filename)) {
xhr.open("GET", url, true);
xhr.onload = function() {
$("#mainText").css("display", "");
$("#mainText").innerHTML("<iframe style=\"width:100%;height:700px;border:0\" src=\"data:text/html," + escape(xhr.responseText) + "\"></iframe>");
};
xhr.send(null);
} else if (!/(jpg|jpeg|png|gif|webp)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) {
xhr.open("GET", url, true);
xhr.onload = function() {
$("#mainText").css("display", "");
$("#mainText").innerText(xhr.responseText);
};
xhr.send(null);
}
};
img.onload = function() {
var h = img.height,
w = img.width,
sw = w,
sh = h;
settings.rotateTimes = (4 + settings.rotateTimes) % 4;
x.save();
if (settings.rotateTimes % 2 === 1) {
sh = w;
sw = h;
}
canvas.height = sh;
canvas.width = sw;
x.translate(sw / 2, sh / 2);
x.rotate(Math.PI / 2 * settings.rotateTimes);
x.translate(-w / 2, -h / 2);
if (settings.vflip) {
x.scale(1, -1);
x.translate(0, -h);
}
if (settings.hflip) {
x.scale(-1, 1);
x.translate(-w, 0);
}
canvas.style.display = "none";
scrollTo(0, 0);
x.drawImage(img, 0, 0);
updateScale(false);
canvas.style.display = "";
$("body").css("overflowY", "");
x.restore();
};
img.src = url;
if ($("body").css("scrollHeight") / innerHeight > 1) {
$("body").css("overflowY", "scroll");
}
var img = new Image();
img.onerror = function() {
canvas.width = innerWidth - 100;
canvas.height = 300;
x.fillStyle = "black";
x.font = "50px sans-serif";
x.strokeStyle = "black";
x.fillText("Page #" + (currentImage + 1) + " (" +
imageFiles[currentImage].filename + ")", innerWidth / 2, 100);
x.fillStyle = "black";
x.fillText("Is corrupt or not an image", innerWidth / 2, 200);
var xhr = new XMLHttpRequest();
if (/(html|htm)$/.test(imageFiles[currentImage].filename)) {
xhr.open("GET", url, true);
xhr.onload = function() {
$("#mainText").css("display", "");
$("#mainText").innerHTML("<iframe style=\"width:100%;height:700px;border:0\" src=\"data:text/html," + escape(xhr.responseText) + "\"></iframe>");
};
xhr.send(null);
} else if (!/(jpg|jpeg|png|gif|webp)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) {
xhr.open("GET", url, true);
xhr.onload = function() {
$("#mainText").css("display", "");
$("#mainText").innerText(xhr.responseText);
};
xhr.send(null);
}
};
img.onload = function() {
var h = img.height,
w = img.width,
sw = w,
sh = h;
settings.rotateTimes = (4 + settings.rotateTimes) % 4;
x.save();
if (settings.rotateTimes % 2 === 1) {
sh = w;
sw = h;
}
canvas.height = sh;
canvas.width = sw;
x.translate(sw / 2, sh / 2);
x.rotate(Math.PI / 2 * settings.rotateTimes);
x.translate(-w / 2, -h / 2);
if (settings.vflip) {
x.scale(1, -1);
x.translate(0, -h);
}
if (settings.hflip) {
x.scale(-1, 1);
x.translate(-w, 0);
}
canvas.style.display = "none";
scrollTo(0, 0);
x.drawImage(img, 0, 0);
canvas.style.display = "";
$("body").css("overflowY", "");
x.restore();
};
img.src = url;
}
}
// reloadImages is a slow process when multiple images are involved. Only used when rotating/mirroring
function reloadImages() {
for(i=0; i < imageFiles.length; i++) {
setImage(imageFiles[i].dataURI, $(".mainImage")[i]);
}
}
@@ -410,9 +426,6 @@ function showPrevPage() {
currentImage++;
} else {
updatePage();
if (settings.nextPage === 0) {
$("#mainContent").scrollTop(0);
}
}
}
@@ -423,36 +436,53 @@ function showNextPage() {
currentImage--;
} else {
updatePage();
if (settings.nextPage === 0) {
$("#mainContent").scrollTop(0);
}
}
}
function updateScale(clear) {
var mainImageStyle = getElem("mainImage").style;
mainImageStyle.width = "";
mainImageStyle.height = "";
mainImageStyle.maxWidth = "";
mainImageStyle.maxHeight = "";
var maxheight = innerHeight - 50;
if (!clear) {
switch (settings.fitMode) {
case kthoom.Key.B:
mainImageStyle.maxWidth = "100%";
mainImageStyle.maxHeight = maxheight + "px";
break;
case kthoom.Key.H:
mainImageStyle.height = maxheight + "px";
break;
case kthoom.Key.W:
mainImageStyle.width = "100%";
break;
default:
break;
}
function scrollCurrentImageIntoView() {
if(settings.pageDisplay == 0) {
// This will scroll all the way up when Single Page is selected
$("#mainContent").scrollTop(0);
} else {
// This will scroll to the image when Long Strip is selected
$("#mainContent").stop().animate({
scrollTop: $(".mainImage").eq(currentImage).offset().top + $("#mainContent").scrollTop() - $("#mainContent").offset().top
}, 200);
}
}
function updateScale() {
var canvasArray = $("#mainContent > canvas");
var maxheight = innerHeight - 50;
canvasArray.css("width", "");
canvasArray.css("height", "");
canvasArray.css("maxWidth", "");
canvasArray.css("maxHeight", "");
if(settings.pageDisplay === 0) {
canvasArray.addClass("hide");
pageDisplayUpdate();
}
switch (settings.fitMode) {
case kthoom.Key.B:
canvasArray.css("maxWidth", "100%");
canvasArray.css("maxHeight", maxheight + "px");
break;
case kthoom.Key.H:
canvasArray.css("maxHeight", maxheight + "px");
break;
case kthoom.Key.W:
canvasArray.css("width", "100%");
break;
default:
break;
}
$("#mainContent > canvas.error").css("width", innerWidth - 100);
$("#mainContent > canvas.error").css("height", 200);
$("#mainContent").css({maxHeight: maxheight + 5});
kthoom.setSettings();
kthoom.saveSettings();
@@ -469,6 +499,20 @@ function keyHandler(evt) {
if (hasModifier) break;
showRightPage();
break;
case kthoom.Key.S:
if (hasModifier) break;
settings.pageDisplay = 0;
pageDisplayUpdate();
kthoom.setSettings();
kthoom.saveSettings();
break;
case kthoom.Key.O:
if (hasModifier) break;
settings.pageDisplay = 1;
pageDisplayUpdate();
kthoom.setSettings();
kthoom.saveSettings();
break;
case kthoom.Key.L:
if (hasModifier) break;
settings.rotateTimes--;
@@ -476,6 +520,7 @@ function keyHandler(evt) {
settings.rotateTimes = 3;
}
updatePage();
reloadImages();
break;
case kthoom.Key.R:
if (hasModifier) break;
@@ -484,6 +529,7 @@ function keyHandler(evt) {
settings.rotateTimes = 0;
}
updatePage();
reloadImages();
break;
case kthoom.Key.F:
if (hasModifier) break;
@@ -499,26 +545,27 @@ function keyHandler(evt) {
settings.hflip = true;
}
updatePage();
reloadImages();
break;
case kthoom.Key.W:
if (hasModifier) break;
settings.fitMode = kthoom.Key.W;
updateScale(false);
updateScale();
break;
case kthoom.Key.H:
if (hasModifier) break;
settings.fitMode = kthoom.Key.H;
updateScale(false);
updateScale();
break;
case kthoom.Key.B:
if (hasModifier) break;
settings.fitMode = kthoom.Key.B;
updateScale(false);
updateScale();
break;
case kthoom.Key.N:
if (hasModifier) break;
settings.fitMode = kthoom.Key.N;
updateScale(false);
updateScale();
break;
case kthoom.Key.SPACE:
if (evt.shiftKey) {
@@ -537,6 +584,43 @@ function keyHandler(evt) {
}
}
function drawCanvas() {
var maxheight = innerHeight - 50;
var canvasElement = $("<canvas></canvas>");
var x = canvasElement[0].getContext("2d");
canvasElement.addClass("mainImage");
switch (settings.fitMode) {
case kthoom.Key.B:
canvasElement.css("maxWidth", "100%");
canvasElement.css("maxHeight", maxheight + "px");
break;
case kthoom.Key.H:
canvasElement.css("maxHeight", maxheight + "px");
break;
case kthoom.Key.W:
canvasElement.css("width", "100%");
break;
default:
break;
}
if(settings.pageDisplay === 0) {
canvasElement.addClass("hide");
}
//Fill with Placeholder text. setImage will override this
canvasElement.width = innerWidth - 100;
canvasElement.height = 200;
x.fillStyle = "black";
x.textAlign = "center";
x.font = "24px sans-serif";
x.strokeStyle = (settings.theme === "dark") ? "white" : "black";
x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100);
$("#mainContent").append(canvasElement);
}
function init(filename) {
var request = new XMLHttpRequest();
request.open("GET", filename);
@@ -548,16 +632,17 @@ function init(filename) {
console.warn(request.statusText, request.responseText);
}
});
kthoom.loadSettings();
setTheme();
updateScale();
request.send();
initProgressClick();
document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : "";
kthoom.loadSettings();
updateScale(true);
$(document).keydown(keyHandler);
$(window).resize(function() {
updateScale(false);
updateScale();
});
// Open TOC menu
@@ -588,28 +673,35 @@ function init(filename) {
value = /^\d+$/.test(value) ? parseInt(value) : value;
settings[this.name] = value;
if(["hflip", "vflip", "rotateTimes"].includes(this.name)) {
reloadImages();
} else if(this.name === "direction") {
return updateProgress();
}
updatePage();
updateScale(false);
updateScale();
});
// Close modal
$(".closer, .overlay").click(function() {
$(".md-show").removeClass("md-show");
$("#mainContent").focus(); // focus back on the main container so you use up/down keys without having to click on it
});
// TOC thumbnail pagination
$("#thumbnails").on("click", "a", function() {
currentImage = $(this).data("page") - 1;
updatePage();
if (settings.nextPage === 0) {
$("#mainContent").scrollTop(0);
}
});
// Fullscreen mode
if (typeof screenfull !== "undefined") {
$("#fullscreen").click(function() {
screenfull.toggle($("#container")[0]);
// Focus on main container so you can use up/down keys immediately after fullscreen
$("#mainContent").focus();
});
if (screenfull.raw) {
@@ -633,7 +725,7 @@ function init(filename) {
showRightPage();
},
});
$("#mainImage").click(function(evt) {
$(".mainImage").click(function(evt) {
// Firefox does not support offsetX/Y so we have to manually calculate
// where the user clicked in the image.
var mainContentWidth = $("#mainContent").width();
@@ -668,5 +760,37 @@ function init(filename) {
showRightPage();
}
});
// Scrolling up/down will update current image if a new image is into view (for Long Strip Display)
$("#mainContent").scroll(function(){
var scroll = $("#mainContent").scrollTop();
if(settings.pageDisplay === 0) {
// Don't trigger the scroll for Single Page
} else if(scroll > prevScrollPosition) {
//Scroll Down
if(currentImage + 1 < imageFiles.length) {
if(currentImageOffset(currentImage + 1) <= 1) {
currentImage++;
scrollTocToActive();
updateProgress();
}
}
} else {
//Scroll Up
if(currentImage - 1 > -1 ) {
if(currentImageOffset(currentImage - 1) >= 0) {
currentImage--;
scrollTocToActive();
updateProgress();
}
}
}
// Update scroll position
prevScrollPosition = scroll;
});
}
function currentImageOffset(imageIndex) {
return $(".mainImage").eq(imageIndex).offset().top - $("#mainContent").position().top
}
+2 -2
View File
File diff suppressed because one or more lines are too long
+9 -3
View File
@@ -177,6 +177,9 @@
whileplaying: function () {
// get csrf_token
let csrf_token = $("input[name='csrf_token']").val();
//This sends a bookmark update to calibreweb every 30 seconds.
if (this.progressBuffer == undefined) {
@@ -187,7 +190,10 @@
$.ajax(calibre.bookmarkUrl, {
method: "post",
data: { bookmark: this.position }
data: {
csrf_token: csrf_token,
bookmark: this.position
}
}).fail(function (xhr, status, error) {
console.error(error);
});
@@ -313,14 +319,14 @@
},
onstop: function () {
$.ajax(calibre.bookmarkUrl, {
method: "post",
data: { bookmark: this.position }
}).fail(function (xhr, status, error) {
console.error(error);
});
utils.css.remove(dom.o, 'playing');
},
@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.gl={days:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"],daysShort:["Dom","Lun","Mar","Mér","Xov","Ven","Sáb"],daysMin:["Do","Lu","Ma","Me","Xo","Ve","Sa"],months:["Xaneiro","Febreiro","Marzo","Abril","Maio","Xuño","Xullo","Agosto","Setembro","Outubro","Novembro","Decembro"],monthsShort:["Xan","Feb","Mar","Abr","Mai","Xun","Xul","Ago","Sep","Out","Nov","Dec"],today:"Hoxe",clear:"Limpar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);
@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.id={days:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],daysShort:["Mgu","Sen","Sel","Rab","Kam","Jum","Sab"],daysMin:["Mg","Sn","Sl","Ra","Ka","Ju","Sa"],months:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Ags","Sep","Okt","Nov","Des"],today:"Hari Ini",clear:"Kosongkan"}}(jQuery);
@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.no={days:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],daysShort:["søn","man","tir","ons","tor","fre","lør"],daysMin:["sø","ma","ti","on","to","fr","lø"],months:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthsShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],today:"i dag",monthsTitle:"Måneder",clear:"Nullstill",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);
@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.vi={days:["Chủ nhật","Thứ hai","Thứ ba","Thứ tư","Thứ năm","Thứ sáu","Thứ bảy"],daysShort:["CN","Thứ 2","Thứ 3","Thứ 4","Thứ 5","Thứ 6","Thứ 7"],daysMin:["CN","T2","T3","T4","T5","T6","T7"],months:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],monthsShort:["Th1","Th2","Th3","Th4","Th5","Th6","Th7","Th8","Th9","Th10","Th11","Th12"],today:"Hôm nay",clear:"Xóa",format:"dd/mm/yyyy"}}(jQuery);
+2 -2
View File
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More