Compare commits

..
293 Commits
Author SHA1 Message Date
Ozzie Isaacs 4da64ceb23 Update to version 0.6.14 2021-10-31 11:31:53 +01: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
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
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
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
471 changed files with 37806 additions and 31165 deletions
+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.
+20 -15
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, 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)
@@ -34,12 +39,19 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
## Quick start
#### Install via pip
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
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\
@@ -50,7 +62,7 @@ Please note that running the above install command can fail on some versions of
## 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 +72,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)
+29
View File
@@ -0,0 +1,29 @@
# 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 ||
| 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 ||
| V 0.6.14|Cross-Site Scripting vulnerability on typeahead inputs. Thanks to @notdodo||
+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
+45 -14
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 os
import mimetypes
@@ -29,7 +28,7 @@ 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
@@ -37,6 +36,17 @@ from . import config_sql, logger, cache_buster, cli, ub, db
from .reverseproxy import ReverseProxied
from .server import WebServer
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 +66,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)
@@ -83,25 +102,37 @@ 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)
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 +156,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 +176,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()
+19 -8
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,6 +28,10 @@ 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 .render_template import render_title_template
@@ -54,6 +57,12 @@ 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__)
@@ -69,6 +78,7 @@ _VERSIONS = OrderedDict(
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__,
@@ -78,13 +88,14 @@ _VERSIONS = OrderedDict(
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
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(uploader.get_versions())
+212 -146
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")
@@ -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,10 @@ 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?')
return json.dumps(texts)
@@ -857,19 +889,11 @@ def list_restriction(res_type, user_id):
else:
json_dumps = ""
js = json.dumps(json_dumps)
response = make_response(js.replace("'", '"'))
response = make_response(js) #.replace("'", '"')
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/pathchooser/")
@login_required
@admin_required
@@ -958,16 +982,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 +1000,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 +1051,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 +1094,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 +1132,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 +1166,50 @@ 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)
_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")
# warning = {'type': "warning", 'message': _(u"DB is not Writeable")}
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 +1231,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 +1262,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 +1271,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):
@@ -1317,6 +1356,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,6 +1376,13 @@ 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.User).filter(ub.User.id == content.id).delete()
ub.session_commit()
log.info(u"User {} deleted".format(content.name))
@@ -1384,6 +1432,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"):
@@ -1439,6 +1494,8 @@ def new_user():
else:
content.role = config.config_default_role
content.sidebar_view = config.config_default_show
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, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check)
@@ -1636,7 +1693,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 +1714,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 +1748,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
@@ -1754,7 +1820,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
+3 -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,9 @@ 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')
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")
@@ -115,5 +101,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")
-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
from . import logger, isoLanguages
+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.14'}
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
+59 -19
View File
@@ -17,7 +17,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 re
@@ -387,8 +386,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 +395,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 +404,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 +524,44 @@ 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.config = config
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)
@@ -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:
@@ -772,11 +806,16 @@ class CalibreDB():
.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)
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
#try:
# if lang.lang_code.lower() == "und":
# lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
# # lang.name = _("Undetermined")
# else:
# cur_l = LC.parse(lang.lang_code)
# lang.name = cur_l.get_language_name(get_locale())
#except UnknownLocaleError:
# lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
return languages
def update_title_sort(self, config, conn=None):
@@ -828,7 +867,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")
+114 -22
View File
@@ -20,21 +20,31 @@
# 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
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
@@ -48,6 +58,8 @@ except ImportError:
pass # We're not using Python 3
editbook = Blueprint('editbook', __name__)
log = logger.create()
@@ -447,9 +459,12 @@ def edit_book_series_index(series_index, book):
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
@@ -506,6 +521,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 +574,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 +671,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 +821,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 +833,8 @@ def edit_book(book_id):
if modif_date:
book.last_modified = datetime.utcnow()
kobo_sync_status.remove_synced_book(edited_books_id)
calibre_db.session.merge(book)
calibre_db.session.commit()
if config.config_use_google_drive:
@@ -1009,7 +1037,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 +1048,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 +1089,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 +1135,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 +1152,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
@@ -1198,3 +1250,43 @@ def merge_list_book():
delete_book(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
+41 -43
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")
@@ -115,46 +113,46 @@ def revoke_watch_gdrive():
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
+2 -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,7 @@ try:
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import text
try:
from apiclient import errors
@@ -168,7 +168,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]:
+30 -37
View File
@@ -17,7 +17,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 io
@@ -38,6 +37,7 @@ 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
from markupsafe import escape
try:
from urllib.parse import quote
@@ -97,10 +97,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 +215,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 +233,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 not 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 +249,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 +327,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 +381,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 +396,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 +489,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):
@@ -707,15 +703,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 +771,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
@@ -838,8 +831,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" % (
-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 .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
+375 -2
View File
@@ -6,8 +6,6 @@
# pylint: disable=too-many-lines,bad-continuation
from __future__ import unicode_literals
# This file is autogenerated, do NOT add, change, or delete ANY string
# If you need help or assistance for adding a new language, please contact the project team
@@ -972,6 +970,8 @@ LANGUAGE_NAMES = {
"gor": "Gorontalo",
"got": "Γοτθικά",
"grb": "Grebo",
"grc": "Ελληνικά; Αρχαία (to 1453)",
"ell": "Ελληνικά; Μοντέρνα (1453-)",
"grn": "Guarani",
"guj": "Gujarati",
"gwi": "Gwichʼin",
@@ -7433,6 +7433,379 @@ LANGUAGE_NAMES = {
"zxx": "No linguistic content",
"zza": "扎扎其语"
},
"zh_Hant_TW": {
"abk": "Abkhazian",
"ace": "亞齊語",
"ach": "Acoli",
"ada": "Adangme",
"ady": "Adyghe",
"aar": "Afar",
"afh": "Afrihili",
"afr": "南非荷蘭文",
"ain": "愛奴語 ( 日本 )",
"aka": "Akan",
"akk": "Akkadian",
"sqi": "阿爾巴尼亞文",
"ale": "Aleut",
"amh": "Amharic",
"anp": "Angika",
"ara": "阿拉伯",
"arg": "Aragonese",
"arp": "Arapaho",
"arw": "Arawak",
"hye": "Armenian",
"asm": "Assamese",
"ast": "Asturian",
"ava": "Avaric",
"ave": "Avestan",
"awa": "Awadhi",
"aym": "Aymara",
"aze": "亞塞拜疆文",
"ban": "Balinese",
"bal": "Baluchi",
"bam": "Bambara",
"bas": "Basa (Cameroon)",
"bak": "Bashkir",
"eus": "巴斯克文",
"bej": "Beja",
"bel": "白俄羅斯文",
"bem": "Bemba (Zambia)",
"ben": "Bengali",
"bho": "Bhojpuri",
"bik": "Bikol",
"byn": "Bilin",
"bin": "Bini",
"bis": "Bislama",
"zbl": "布利斯符號",
"bos": "Bosnian",
"bra": "Braj",
"bre": "布列塔尼文",
"bug": "Buginese",
"bul": "保加利亞文",
"bua": "Buriat",
"mya": "Burmese",
"cad": "Caddo",
"cat": "加泰隆文",
"ceb": "Cebuano",
"chg": "Chagatai",
"cha": "Chamorro",
"che": "Chechen",
"chr": "Cherokee",
"chy": "Cheyenne",
"chb": "Chibcha",
"zho": "中文",
"chn": "Chinook jargon",
"chp": "Chipewyan",
"cho": "Choctaw",
"chk": "Chuukese",
"chv": "Chuvash",
"cop": "Coptic",
"cor": "Cornish",
"cos": "Corsican",
"cre": "Cree",
"mus": "Creek",
"hrv": "克羅地亞文",
"ces": "捷克文",
"dak": "Dakota",
"dan": "丹麥文",
"dar": "Dargwa",
"del": "Delaware",
"div": "Dhivehi",
"din": "Dinka",
"doi": "Dogri (macrolanguage)",
"dgr": "Dogrib",
"dua": "Duala",
"nld": "荷蘭文",
"dyu": "Dyula",
"dzo": "Dzongkha",
"efi": "Efik",
"egy": "Egyptian (Ancient)",
"eka": "Ekajuk",
"elx": "Elamite",
"eng": "英文",
"myv": "Erzya",
"epo": "世界語",
"est": "愛沙尼亞文",
"ewe": "Ewe",
"ewo": "Ewondo",
"fan": "Fang (Equatorial Guinea)",
"fat": "Fanti",
"fao": "法羅文",
"fij": "Fijian",
"fil": "Filipino",
"fin": "芬蘭文",
"fon": "Fon",
"fra": "法文",
"fur": "Friulian",
"ful": "Fulah",
"gaa": "Ga",
"glg": "Galician",
"lug": "Ganda",
"gay": "Gayo",
"gba": "Gbaya (Central African Republic)",
"gez": "Geez",
"kat": "Georgian",
"deu": "德文",
"gil": "Gilbertese",
"gon": "Gondi",
"gor": "Gorontalo",
"got": "Gothic",
"grb": "Grebo",
"grn": "Guarani",
"guj": "古吉拉特語",
"gwi": "Gwichʼin",
"hai": "Haida",
"hau": "Hausa",
"haw": "Hawaiian",
"heb": "希伯來",
"her": "Herero",
"hil": "Hiligaynon",
"hin": "Hindi",
"hmo": "Hiri Motu",
"hit": "Hittite",
"hmn": "Hmong",
"hun": "匈牙利文",
"hup": "Hupa",
"iba": "Iban",
"isl": "冰島文",
"ido": "Ido",
"ibo": "Igbo",
"ilo": "Iloko",
"ind": "印尼文",
"inh": "Ingush",
"ina": "Interlingua (International Auxiliary Language Association)",
"ile": "Interlingue",
"iku": "Inuktitut",
"ipk": "Inupiaq",
"gle": "愛爾蘭文",
"ita": "意大利文",
"jpn": "日本",
"jav": "Javanese",
"jrb": "Judeo-Arabic",
"jpr": "Judeo-Persian",
"kbd": "Kabardian",
"kab": "Kabyle",
"kac": "Kachin",
"kal": "Kalaallisut",
"xal": "Kalmyk",
"kam": "Kamba (Kenya)",
"kan": "Kannada",
"kau": "Kanuri",
"kaa": "Kara-Kalpak",
"krc": "Karachay-Balkar",
"krl": "Karelian",
"kas": "Kashmiri",
"csb": "Kashubian",
"kaw": "Kawi",
"kaz": "Kazakh",
"kha": "Khasi",
"kho": "Khotanese",
"kik": "Kikuyu",
"kmb": "Kimbundu",
"kin": "Kinyarwanda",
"kir": "Kirghiz",
"tlh": "Klingon",
"kom": "Komi",
"kon": "Kongo",
"kok": "Konkani (macrolanguage)",
"kor": "韓國",
"kos": "Kosraean",
"kpe": "Kpelle",
"kua": "Kuanyama",
"kum": "Kumyk",
"kur": "Kurdish",
"kru": "Kurukh",
"kut": "Kutenai",
"lad": "Ladino",
"lah": "Lahnda",
"lam": "Lamba",
"lao": "Lao",
"lat": "Latin",
"lav": "拉脫維亞文",
"lez": "Lezghian",
"lim": "Limburgan",
"lin": "Lingala",
"lit": "立陶宛文",
"jbo": "Lojban",
"loz": "Lozi",
"lub": "Luba-Katanga",
"lua": "Luba-Lulua",
"lui": "Luiseno",
"smj": "Lule Sami",
"lun": "Lunda",
"luo": "Luo (Kenya and Tanzania)",
"lus": "Lushai",
"ltz": "Luxembourgish",
"mkd": "馬其頓文",
"mad": "Madurese",
"mag": "Magahi",
"mai": "Maithili",
"mak": "Makasar",
"mlg": "Malagasy",
"msa": "Malay (macrolanguage)",
"mal": "Malayalam",
"mlt": "Maltese",
"mnc": "Manchu",
"mdr": "Mandar",
"man": "Mandingo",
"mni": "Manipuri",
"glv": "Manx",
"mri": "Maori",
"arn": "Mapudungun",
"mar": "Marathi",
"chm": "Mari (Russia)",
"mah": "Marshallese",
"mwr": "Marwari",
"mas": "Masai",
"men": "Mende (Sierra Leone)",
"mic": "Mi'kmaq",
"min": "Minangkabau",
"mwl": "Mirandese",
"moh": "Mohawk",
"mdf": "Moksha",
"lol": "Mongo",
"mon": "Mongolian",
"mos": "Mossi",
"mul": "Multiple languages",
"nqo": "N'Ko",
"nau": "Nauru",
"nav": "Navajo",
"ndo": "Ndonga",
"nap": "Neapolitan",
"nia": "Nias",
"niu": "Niuean",
"zxx": "非語言內容",
"nog": "Nogai",
"nor": "挪威文",
"nob": "Norwegian Bokmål",
"nno": "Norwegian Nynorsk",
"nym": "Nyamwezi",
"nya": "Nyanja",
"nyn": "Nyankole",
"nyo": "Nyoro",
"nzi": "Nzima",
"oci": "Occitan (post 1500)",
"oji": "Ojibwa",
"orm": "Oromo",
"osa": "Osage",
"oss": "Ossetian",
"pal": "Pahlavi",
"pau": "Palauan",
"pli": "Pali",
"pam": "Pampanga",
"pag": "Pangasinan",
"pan": "Panjabi",
"pap": "Papiamento",
"fas": "Persian",
"phn": "Phoenician",
"pon": "Pohnpeian",
"pol": "波蘭文",
"por": "葡萄牙文",
"pus": "Pashto",
"que": "Quechua",
"raj": "Rajasthani",
"rap": "Rapanui",
"ron": "羅馬尼亞文",
"roh": "Romansh",
"rom": "Romany",
"run": "Rundi",
"rus": "俄羅斯文",
"smo": "Samoan",
"sad": "Sandawe",
"sag": "Sango",
"san": "Sanskrit",
"sat": "Santali",
"srd": "Sardinian",
"sas": "Sasak",
"sco": "Scots",
"sel": "Selkup",
"srp": "塞爾維亞文",
"srr": "Serer",
"shn": "Shan",
"sna": "Shona",
"scn": "Sicilian",
"sid": "Sidamo",
"bla": "Siksika",
"snd": "Sindhi",
"sin": "Sinhala",
"den": "Slave (Athapascan)",
"slk": "斯洛伐克文",
"slv": "斯洛文尼亞文",
"sog": "Sogdian",
"som": "Somali",
"snk": "Soninke",
"spa": "西班牙文",
"srn": "Sranan Tongo",
"suk": "Sukuma",
"sux": "Sumerian",
"sun": "Sundanese",
"sus": "Susu",
"swa": "Swahili (macrolanguage)",
"ssw": "Swati",
"swe": "瑞典文",
"syr": "Syriac",
"tgl": "Tagalog",
"tah": "Tahitian",
"tgk": "Tajik",
"tmh": "Tamashek",
"tam": "Tamil",
"tat": "Tatar",
"tel": "Telugu",
"ter": "Tereno",
"tet": "Tetum",
"tha": "泰國",
"bod": "Tibetan",
"tig": "Tigre",
"tir": "Tigrinya",
"tem": "Timne",
"tiv": "Tiv",
"tli": "Tlingit",
"tpi": "Tok Pisin",
"tkl": "Tokelau",
"tog": "Tonga (Nyasa)",
"ton": "Tonga (Tonga Islands)",
"tsi": "Tsimshian",
"tso": "Tsonga",
"tsn": "Tswana",
"tum": "Tumbuka",
"tur": "土耳其",
"tuk": "Turkmen",
"tvl": "Tuvalu",
"tyv": "Tuvinian",
"twi": "Twi",
"udm": "Udmurt",
"uga": "Ugaritic",
"uig": "Uighur",
"ukr": "烏克蘭語",
"umb": "Umbundu",
"mis": "Uncoded languages",
"und": "Undetermined",
"urd": "烏爾都語",
"uzb": "烏茲別克語",
"vai": "Vai",
"ven": "文達語",
"vie": "越南語",
"vol": "沃拉普克語",
"vot": "Votic",
"wln": "瓦隆語",
"war": "Waray (Philippines)",
"was": "Washo",
"cym": "威爾斯語",
"wal": "Wolaytta",
"wol": "渥魯夫語",
"xho": " 科薩語",
"sah": "Yakut",
"yao": "Yao",
"yap": "雅浦語",
"yid": "意第緒語",
"yor": "約魯巴語",
"zap": "薩波特克語",
"zza": "扎扎其語",
"zen": " 哲納加語",
"zha": " 壯語",
"zul": "祖魯語",
"zun": "祖尼語"
},
"en": {
"aar": "Afar",
"abk": "Abkhazian",
+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()
+170 -65
View File
@@ -19,7 +19,6 @@
import base64
import datetime
import sys
import os
import uuid
from time import gmtime, strftime
@@ -42,11 +41,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 +83,7 @@ CONNECTION_SPECIFIC_HEADERS = [
"transfer-encoding",
]
def get_kobo_activated():
return config.config_kobo_sync
@@ -135,6 +138,7 @@ 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')
@@ -151,32 +155,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, db.Books.id == ub.ArchivedBook.book_id)
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
ub.KoboSyncedBooks.book_id == None))
.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, db.Books.id == ub.ArchivedBook.book_id)
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
ub.KoboSyncedBooks.book_id == None))
.filter(calibre_db.common_filters())
.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)
for book in books:
kobo_sync_status.add_synced_books(book.Books.id)
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 +225,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 +240,59 @@ 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)
if sqlalchemy_version2:
max_change = calibre_db.session.execute(changed_entries
.filter(ub.ArchivedBook.is_archived)
.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) \
.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)))
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,15 +302,15 @@ 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
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
# 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):
@@ -270,6 +333,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers["x-kobo-sync"] = "continue"
sync_token.to_headers(extra_headers)
# log.debug("Kobo Sync Content: {}".format(sync_results))
response = make_response(jsonify(sync_results), extra_headers)
return response
@@ -394,7 +458,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,
@@ -422,10 +486,7 @@ 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() ?
@@ -436,7 +497,7 @@ def get_metadata(book):
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 +532,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 +588,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 +618,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 +665,14 @@ 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):
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
):
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
sync_results.append({
"DeletedTag": {
"Tag": {
@@ -616,9 +681,44 @@ 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 +763,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):
@@ -838,6 +938,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 +961,11 @@ def HandleBookDeletionRequest(book_uuid):
ub.session.merge(archived_book)
ub.session_commit()
return ("", 204)
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 +973,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,6 +984,7 @@ def HandleUserRequest(dummy=None):
return redirect_or_proxy_request()
@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"])
@@ -914,6 +1018,7 @@ def make_calibre_web_auth_response():
)
@csrf.exempt
@kobo.route("/v1/auth/device", methods=["POST"])
@requires_kobo_auth
def HandleAuthRequest():
+71
View File
@@ -0,0 +1,71 @@
# -*- 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_
def add_synced_books(book_id):
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()
def remove_synced_book(book_id):
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).delete()
ub.session_commit()
def add_archived_books(book_id):
archived_book = (
ub.session.query(ub.ArchivedBook)
.filter(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 = True
archived_book.last_modified = datetime.datetime.utcnow()
ub.session.merge(archived_book)
ub.session_commit()
# select all books which are synced by the current user and do not belong to a synced shelf and them to archive
# select all shelfs from current user which are synced and do not belong to the "only sync" shelfs
def update_on_sync_shelfs(content_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 == content_id, isouter=True)
.filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None))
.filter(ub.KoboSyncedBooks.user_id == content_id).all())
for b in books_to_archive:
add_archived_books(b.book_id)
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == b.book_id).filter(ub.KoboSyncedBooks.user_id == content_id).delete()
ub.session_commit()
shelfs_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == content_id).filter(
ub.Shelf.kobo_sync == 0).all()
for a in shelfs_to_archive:
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=content_id))
ub.session_commit()
-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 sys
import inspect
+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, __):
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()['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, __):
if self.active:
val = list()
result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+"))
for r in result.json()['items']:
v = dict()
v['id'] = r['id']
v['title'] = r['volumeInfo']['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
+61
View File
@@ -0,0 +1,61 @@
# -*- 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'] = "1234" # publication['bib'].get('title')
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['ratings'] = 0
v['series'] = ""
v['cover'] = generic_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 -14
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 datetime
from functools import wraps
@@ -433,16 +432,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 +536,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
-1
View File
@@ -27,7 +27,6 @@
# http://flask.pocoo.org/snippets/62/
from __future__ import division, print_function, unicode_literals
try:
from urllib.parse import urlparse, urljoin
except ImportError:
-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:
static_cover = url_for('static', filename='generic_cover.jpg')
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
meta = {executor.submit(c.search, query, static_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
+20 -14
View File
@@ -35,10 +35,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 +82,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 +94,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 +103,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 +138,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 +152,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 +176,16 @@ 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.raw_kobo_store_token,
self.books_last_created,
self.books_last_modified,
self.archive_last_modified,
self.reading_state_last_modified,
self.tags_last_modified)
#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
+24 -5
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
@@ -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
+52 -42
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()
@@ -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()
@@ -228,38 +225,42 @@ def remove_from_shelf(shelf_id, book_id):
@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()
shelf.is_public = 1 if to_save.get("is_public") else 0
if config.config_kobo_sync:
shelf.kobo_sync = True if to_save.get("kobo_sync") else False
shelf_title = to_save.get("title", "")
if check_shelf_is_unique(shelf, shelf_title, shelf_id):
shelf.name = shelf_title
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 +272,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, shelf_id=False):
if shelf_id:
ident = ub.Shelf.id != shelf_id
else:
ident = true()
if shelf.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
@@ -362,8 +368,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 +378,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 +410,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 +422,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.
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