Compare commits

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

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