Compare commits

..
560 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 54d06e580d Release version 0.6.12 2021-05-22 14:08:46 +02:00
Ozzie Isaacs a62fca1e55 Merge branch 'master' into Develop 2021-05-22 11:28:59 +02:00
Ozzie Isaacs 7b1d23eba4 Restored Discord Widget 2021-05-22 10:03:48 +02:00
Ozzie Isaacs 2502c279fc Update discord 2021-05-22 09:57:47 +02:00
Ozzie Isaacs 878fc8c2fd Added download stat badge 2021-05-21 20:58:10 +02:00
Ozzie Isaacs 64902eebcb Merge remote-tracking branch 'batches/patch-2' into master 2021-05-21 20:54:06 +02:00
flying-sausages e520f6afc4 Add Shields to README.md 2021-05-20 22:48:51 +02:00
Ozzie Isaacs 0cc1ae9324 Updated Testresult for Linux 2021-05-20 17:54:25 +02:00
cbartondock 1b2124aa35 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-05-19 16:47:19 -04:00
Ozzie Isaacs bd4fde9e63 Fix #1992 handle invalid numbers of books seriesindex 2021-05-19 19:54:44 +02:00
Ozzie Isaacs c85cfa90a4 Update testresults on windows
Added encoding on fb2 upload
2021-05-19 19:21:09 +02:00
Ozzie Isaacs a0449b50c8 Downgrade flask -> to slow during reboot 2021-05-17 17:54:43 +02:00
Ozzie Isaacs 010c0bfc7d Improved error handling for content too long 2021-05-16 14:07:16 +02:00
Ozzie Isaacs 394b063b8c Fixes for caliblur theme 2021-05-16 13:25:29 +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 b62d84424e Merge remote-tracking branch 'comic/comic_viewer_changes' into master 2021-05-16 13:19:28 +02:00
Ozzie Isaacs e15324d2cd Merge branch 'master' into Develop 2021-05-16 09:38:00 +02:00
Ozzie Isaacs 20b84a9459 Unified translations
Update translations for release
2021-05-16 09:37:45 +02:00
Ozzie Isaacs 109198a374 Merge branch 'master' into Develop 2021-05-16 09:03:58 +02:00
Ozzie Isaacs dafc68f049 Updated requirements 2021-05-16 09:03:47 +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 4745fc0db1 Bugfix search for publish dates and custom column dates
Bugfix save umplauts in comments unescaped (as calibre does)
Improved testability
2021-05-15 07:51:32 +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
Ozzie Isaacs b009dfe4ee Always use bootstrap datepicker 2021-05-14 19:58:06 +02:00
Ozzie Isaacs eba94f430c Updated js libs 2021-05-14 09:29:24 +02:00
Ozzie Isaacs 06347b7e3c Bugfixes from testrun 2021-05-14 08:26:56 +02:00
Ozzie Isaacs 6bc426b21d Fix edit_books.js 2021-05-14 07:29:48 +02:00
cbartondock e0fac8d2c0 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-05-13 11:25:59 -04:00
Ozzie Isaacs 4a849aab7f Merge remote-tracking branch 'requirements/patch-1' into master 2021-05-13 16:28:54 +02:00
Ozzie Isaacs ad2d0c84e4 Update swedish translation 2021-05-13 16:28:02 +02:00
Ozzie Isaacs 6bf360fbfb Added "comments" type to supported custom columns 2021-05-13 14:00:01 +02:00
Ozzie Isaacs 380292a8aa Custom column datetime now searchable (#1984) 2021-05-13 13:31:36 +02:00
jonyxx-alt f9949da745 Update Swedish translation
Upload files that has been updated
2021-05-13 11:04:57 +00:00
Ozzie Isaacs b0cc52e0aa Enable custom column datetime (adv. search not working yet) #1984 2021-05-13 10:39:36 +02:00
Ozzie Isaacs 9ef705650b Access pages with stored order per default (#1987)
Fix for toggle-text of enable-sort button on shelf page
2021-05-13 09:41:27 +02:00
Ozzie Isaacs c6dadbe75e restrictions are now applied to kobo sync requests (Fix #1938) 2021-05-13 09:17:01 +02:00
cbartondock 0a65c05083 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-05-07 09:05:27 -04:00
Robert Schütz 21026340ad remove singledispatch from requirements
It's not used anywhere.
2021-05-05 14:30:00 +02: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
Ozzieisaacs 541c8c4b93 Improved error handling for disapearing custom column linked to visiblility restrictions 2021-05-01 20:52:48 +02:00
Ozzieisaacs b97373bf37 Improved error handling for disapearing custom column linked to read status 2021-05-01 18:42:57 +02:00
Ozzieisaacs c0b561cb5a Better input check for custom_columns 2021-05-01 17:10:29 +02:00
Ozzieisaacs bd3ccfd0a9 Merge remote-tracking branch 'cn/master' into master 2021-05-01 16:33:47 +02:00
Ozzieisaacs 5470acd3af Fix #1964 (RefreshError with unconfigured gdrive) 2021-05-01 12:45:28 +02:00
Ozzie Isaacs 64696fe973 Bugfixes edit user list (Fix #1938) 2021-05-01 08:36:39 +02:00
cbartondock f2cd93dc54 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-29 10:01:18 -04:00
Ozzie Isaacs ed2fa4cdd8 Update error handling for user list (#1938) 2021-04-28 20:49:16 +02:00
cbartondock c2c4636961 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-28 08:43:44 -04:00
dalin 03cca1d1c0 update Simplified Chinese 2021-04-28 16:15:21 +08:00
dalin 354b075885 Merge remote-tracking branch 'upstream/master' 2021-04-28 14:15:42 +08:00
Ozzie Isaacs de7f039b99 Update list user editor (#1938) 2021-04-27 20:01:17 +02:00
cbartondock c2e530af17 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-26 13:49:25 -04:00
Ozzie Isaacs 19797d5d23 Merge remote-tracking branch 'it/patch-24' into master 2021-04-26 19:05:25 +02:00
Ozzie Isaacs 144c2b5fc7 Renaming ipadress - ip_address
Bugfix user table
Result testrun
Updated cn translation
2021-04-26 19:03:01 +02:00
cbartondock 577b525508 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-26 10:23:24 -04:00
GarcaMan 7da40d1c2e Added option to remove Y-Scrollbar and reset page scroll 2021-04-25 19:59:53 +00:00
Ozzie Isaacs 4e3a5ca33b Added additional debug messages, catch additional errors in updater 2021-04-25 11:30:53 +02:00
Ozzie Isaacs 97e4707f72 user list column value and tags editor (#1938)
Bugfixes for massedit in user list (#1938)
Changed no. of debug messages
2021-04-25 10:43:01 +02:00
cbartondock 541dd6b579 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-22 18:12:19 -04:00
Ozzie Isaacs 450ee43677 Additional logging for emails 2021-04-22 19:14:56 +02:00
cbartondock e57229e670 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-22 12:22:45 -04:00
Ozzie Isaacs c0b2e886d2 bugfix books list with selected books and stored on state 2021-04-21 19:28:22 +02:00
Ozzie Isaacs bb4749c65b Result for testrun
Added mass delete of users
refactores user table refresh
Bugfix for sorting with selected users
Bugfix delete books #1938
2021-04-21 19:23:56 +02:00
cbartondock 1ef2a96465 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-19 12:20:54 -04:00
ElQuimm 5aa37c68a2 Updated version of italian.po
Thank you.
2021-04-19 10:11:26 +02:00
Ozzie Isaacs 6e5a1a1f4d Merge remote-tracking branch 'add_Babelio/patch-1' into master 2021-04-18 12:05:00 +02:00
Ozzie Isaacs b624ce16c3 Merge remote-tracking branch 'it/patch-23' into master 2021-04-18 12:04:08 +02:00
Ozzie Isaacs 3be5b5f919 Merge remote-tracking branch 'comic_reader/master' into master 2021-04-18 12:02:21 +02:00
Ozzie Isaacs 04ac5b69ac Further improvements on user list #1938 2021-04-18 12:01:11 +02:00
Ozzie Isaacs 9cc14ac5c7 Fix for #1905 prevent invalid json data in gmail_token 2021-04-18 11:33:14 +02:00
BuildTools 755eb1405b Fix progress bar direction 2021-04-18 04:25:45 +09:00
subdiox 0138294b36 Fix an issue that space key doesn't work intentionally when inverting reading direction 2021-04-18 03:20:41 +09:00
Ozzie Isaacs c0a4addf30 Cancel button in User edit now leads to the right page back #1938 2021-04-17 18:30:55 +02:00
Ozzie Isaacs 39bbee0eeb Books sort with non Books table column working #1938 2021-04-17 10:27:30 +02:00
Ozzie Isaacs 39dda3f534 Fix opds login with colon in password #1952 2021-04-15 18:02:52 +02:00
Ozzie Isaacs 1cb8dbe795 Update migration routine 2021-04-15 17:42:39 +02:00
cbartondock 85f1ca6101 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-15 08:43:28 -04:00
Ozzie Isaacs e13820bbf0 Update exception upon migration of database #1935 2021-04-15 12:05:03 +02:00
malletfils 3973362457 Update db.py
Just adding support for Babelio (french website about books) in the link section
2021-04-14 19:57:02 +02:00
cbartondock 2f3f13afb9 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-14 09:30:05 -04:00
Ozzie Isaacs b38877e193 Document change 2021-04-13 19:47:55 +02:00
Ozzie Isaacs 0e1dbb5377 Copy author names for displaying (#1935) 2021-04-13 19:41:44 +02:00
Ozzie Isaacs f07cc8b103 Update optional-requirements: flask-dance
Catch error for invalid oauth tokens
Fixes for displaying error messages on deleting books from list
Fixes for displaying error messages on deleting bookformats
Removed non working sorting in books list
2021-04-13 19:26:10 +02:00
Ozzie Isaacs 67775bc797 Update requirements
Catch error for invalid oauth tokens
Fixes for displaying error messages on deleting books from list
Fixes for displaying error messages on deleting bookformats
2021-04-13 19:08:02 +02:00
ElQuimm 05933a5f0c Updated italian message.po
I have not removed the #, fuzzy lines, but modified the corresponding values.
2021-04-13 18:51:50 +02:00
cbartondock e15ebd5aac Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-12 19:02:26 -04:00
Ozzie Isaacs d32b2ca524 Prevent traceback after delete user
flash message in case last admin role is removed #1938
2021-04-12 19:04:27 +02:00
Ozzie Isaacs d0a895628e Prevent delete of Guest user #1938 2021-04-12 18:45:44 +02:00
Ozzie Isaacs 90f2b3fb21 Refactored list checkbox sort 2021-04-12 18:39:09 +02:00
Ozzie Isaacs 0f95800dde Update sqlalchemy-utils dependency for oauth
Sort for state checkbox in user list and books list working
2021-04-12 18:23:25 +02:00
cbartondock ff99cd2456 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-11 16:26:55 -04:00
Ozzieisaacs 04971f8672 Bugfix wrong js file in user list 2021-04-11 20:01:40 +02:00
Ozzieisaacs b6177b27f4 Sorting of users in table according to selection possible 2021-04-11 19:59:20 +02:00
Ozzieisaacs ae97e87506 Delete user working from user table (#1938)
Comment in helper
2021-04-10 11:32:25 +02:00
cbartondock df91ca500f Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-08 20:25:02 -04:00
Ozzie Isaacs 2d73f541c0 Bugfix sort books list and user list
Prevent transferring password hash to client
2021-04-08 19:37:08 +02:00
Ozzie Isaacs 7561eabe52 Implement Backend to deny editing Guest rights restriction #1938 2021-04-07 18:56:17 +02:00
Ozzie Isaacs 067fb1b0b7 Prevent delete Guest user and redirect to admin page after user delete 2021-04-07 18:47:48 +02:00
Ozzie Isaacs 78071841cc After Deleting a book the list page is still displayed #1938 2021-04-07 18:19:48 +02:00
Ozzie Isaacs fac232229e Added missing ead/unread category in user list #1938 2021-04-07 17:49:19 +02:00
Ozzie Isaacs a43021e87c Fix for #1938 2021-04-07 17:46:17 +02: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
Ozzie Isaacs 1a0de3b3cf Merge remote-tracking branch 'de_translation/patch-1' into master 2021-04-06 18:08:32 +02:00
Ozzie Isaacs 947154dc9c Merge branch 'development' into master 2021-04-06 18:06:25 +02:00
Ozzie Isaacs 8acd1f1fe4 Code refactoring and improved error handling for edit user list
Update teststatus
2021-04-06 17:29:42 +02:00
Ozzie Isaacs 665210e506 Fixes for book edit with title sort and author sort 2021-04-04 14:01:06 +02:00
Ozzie Isaacs 91b9370a21 Merge branch 'master' into development
# Conflicts:
#	cps/static/css/libs/bootstrap-table.min.css
#	cps/static/js/libs/bootstrap-table/bootstrap-table-editable.min.js
#	cps/static/js/libs/bootstrap-table/bootstrap-table.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-af-ZA.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ar-SA.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-bg-BG.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ca-ES.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-cs-CZ.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-da-DK.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-de-DE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-el-GR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-en-US.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-AR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-CL.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-CR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-ES.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-MX.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-NI.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-SP.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-et-EE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-eu-EU.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fa-IR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fi-FI.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr-BE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr-CH.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr-FR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr-LU.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-he-IL.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-hr-HR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-hu-HU.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-id-ID.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-it-IT.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ja-JP.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ka-GE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ko-KR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ms-MY.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-nb-NO.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-nl-BE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-nl-NL.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-pl-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-pt-PT.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ro-RO.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ru-RU.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sk-SK.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sr-Cyrl-RS.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sr-Latn-RS.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sv-SE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-th-TH.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-tr-TR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-uk-UA.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ur-PK.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-uz-Latn-UZ.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-vi-VN.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-zh-CN.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-zh-TW.min.js
2021-04-02 17:13:22 +02:00
Ozzie Isaacs ca0aa600b4 Bugfix german translation 2021-04-02 17:12:26 +02:00
Ozzie Isaacs 1c42c4c969 Update bootstrap-table
Fixes for book edit
2021-04-02 17:11:16 +02:00
cbartondock 389f0ae452 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-02 10:41:25 -04:00
Ozzie Isaacs 6e406311c3 Fix #1928 (ldap_import_user_filter missing in to_save) 2021-04-02 08:40:29 +02: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
robochud 87884c1af2 Update messages.po
very little typo
2021-03-31 00:56:12 +02:00
Ozzie Isaacs c0623a949c Merge remote-tracking branch 'caliblur/master' into master 2021-03-29 20:54:04 +02:00
cbartondock 42a23ea23c removed some dumb graphical inconsistencies 2021-03-29 14:35:48 -04:00
cbartondock 710c31d1ca Fixed book details not loading after infinite scroll 2021-03-29 14:13:22 -04:00
cbartondock f637ba4dad Fixed border display in book details 2021-03-29 13:52:09 -04:00
Ozzie Isaacs 80d2c60cef Update testresults 2021-03-29 18:03:09 +02:00
Ozzie Isaacs b17d71e4c3 Update optional-requirements.txt
Bugfix Mail task after convert task
2021-03-28 20:36:08 +02:00
Ozzie Isaacs 3c5bd3a605 Added gmail.json file to excluded file, added file also to excluded file during update 2021-03-28 15:01:56 +02:00
Ozzie Isaacs 7cc8d1e693 remove file 2021-03-28 14:58:07 +02:00
Ozzie Isaacs 99520d54a5 Added ability to send emails via gmail (#1905)
Gmail email sending
2021-03-28 14:51:01 +02:00
Ozzie Isaacs e10a8c078b Merge branch 'master' into development
# Conflicts:
#	cps/templates/email_edit.html
2021-03-27 18:36:11 +01:00
Ozzie Isaacs 7a196fed7c Prevent empty email port and server address(#1906) 2021-03-27 18:34:57 +01:00
Ozzie Isaacs c418c7e725 Prevent empty email from field (#1906) 2021-03-27 18:31:49 +01:00
Ozzie Isaacs e35748deff Fix wording and tabs 2021-03-27 17:30:54 +01:00
Ozzie Isaacs b57efbe31c Fix wording and tabs 2021-03-27 17:30:10 +01:00
Ozzie Isaacs 8f91437701 Implementation for gmail server with OAuth2 started 2021-03-27 16:36:24 +01:00
Ozzie Isaacs a7eb547ca4 Renamed brazilian datepicker file 2021-03-27 12:40:23 +01:00
Ozzie Isaacs 0ceb12f74f Renamed brazilian datepicker file 2021-03-27 12:40:01 +01:00
Ozzie Isaacs 4960624de7 Merge branch 'master' into development 2021-03-27 12:20:28 +01:00
Ozzie Isaacs 2c92f24d89 Added Brazilian translation 2021-03-27 12:20:07 +01:00
Ozzie Isaacs 0b7c679dba Changed test email message (fix #657) 2021-03-27 09:48:43 +01:00
Ozzie Isaacs aab3cdc58a Changed test email message 2021-03-27 09:47:43 +01:00
Ozzie Isaacs 3c63f5b204 Merge branch 'master' into development 2021-03-27 09:46:12 +01:00
Ozzie Isaacs 0b97bbf827 Merge remote-tracking branch 'div_a/use-kobo-for-download-link' into master 2021-03-27 09:39:44 +01:00
Ozzie Isaacs 970dbb0c59 Merge branch 'master' into development
# Conflicts:
#	cps/opds.py
#	cps/templates/admin.html
2021-03-27 09:38:12 +01:00
Ozzie Isaacs d21d3c2ceb Merge remote-tracking branch 'div_a/use-btn-anchors' into master 2021-03-27 09:33:28 +01:00
Ozzie Isaacs 0f6493b8ce Update Teststatus 2021-03-27 09:30:59 +01:00
Ozzie Isaacs f26beec1d3 Updated pdf reader 2021-03-25 18:57:40 +01:00
Ozzie Isaacs c4225e29ed Updated pdf reader 2021-03-25 18:57:25 +01:00
Ozzie Isaacs 9243326cd3 Updated testresult 2021-03-25 18:30:31 +01:00
Ozzie Isaacs 86beb8023a Downgrade sqlachemy, as it breaks oauth 2021-03-24 18:47:59 +01:00
Ozzie Isaacs f4e134742b Fix updater
Bugfixes from testruns
Update Testresults
2021-03-24 18:46:03 +01:00
Ozzie Isaacs 2b17bf4114 Update tests
Fixes upload book
2021-03-23 18:57:04 +01:00
Ozzie Isaacs c4f0fc8f7b Update tests
Fixes upload book
2021-03-23 17:57:49 +01:00
Ozzie Isaacs 3d6c836e7d Update tests 2021-03-23 17:39:51 +01:00
Gavin Mogan c279055af4 Use the kobo auth'd version for download link when proxied -- Fixes #1908 2021-03-22 18:08:41 -07:00
Gavin Mogan 657cba042a Use btn classes on anchors not div so the entire button is clickable 2021-03-22 17:25:38 -07:00
Ozzie Isaacs 7a58e48cae Bugfixes opds feed 2021-03-22 19:01:18 +01:00
Ozzie Isaacs 670eab62bf Update opds feed with letters for all titles, authors, categories and series 2021-03-22 17:46:15 +01:00
Ozzie Isaacs fc85586809 Bugfixes for sqlalchemy 1.4.0 2021-03-22 16:33:21 +01:00
Ozzie Isaacs 837fc4988d Letterize authors 2021-03-21 20:21:10 +01:00
Ozzie Isaacs 4664b47851 Fixed alphabetical order in list and grid view
Completed download section
2021-03-21 19:31:32 +01:00
Ozzie Isaacs 9864d932e0 Changed user.nickname to user.name
Added function to view downloads of all users for admins
2021-03-21 18:55:02 +01:00
Ozzie Isaacs 436f60caa9 refactored some functions 2021-03-21 14:17:07 +01:00
Ozzie Isaacs e9530eda9d Bugfix from refactoring 2021-03-21 13:46:13 +01:00
Ozzie Isaacs f4ddac16f9 Improved error logging for #1905 2021-03-21 11:58:51 +01:00
Ozzie Isaacs 33bdc07f55 Fix for #1845 (ods not working in Moonreader an Librera)
Fix opds search with wrong parameter no longer causes error 500
2021-03-21 11:57:42 +01:00
Ozzie Isaacs 130a4ed2d3 Fix opds and error logging in mail 2021-03-21 11:54:39 +01:00
Ozzie Isaacs 59ebc1af8a Code refactoring 2021-03-21 08:20:04 +01:00
Ozzie Isaacs 82e15d2e98 Merge branch 'master' into development
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2021-03-21 07:28:52 +01:00
Ozzie Isaacs 9c842f1895 Bugfixes pdf xmp parsing 2021-03-21 07:28:35 +01:00
Ozzie Isaacs 8cc849488b Code cosmetics 2021-03-21 07:15:38 +01:00
Ozzie Isaacs fcaa232967 Merge remote-tracking branch 'origin/master' into master 2021-03-21 07:11:37 +01:00
Ozzie Isaacs d3f8153b90 Merge branch 'master' into development 2021-03-20 19:34:16 +01:00
Ozzie Isaacs 99ecf22790 Merge remote-tracking branch 'origin/master' into master 2021-03-20 18:10:42 +01:00
Ozzie Isaacs b1b7ee65b4 Refactored advanced search 2021-03-20 18:10:27 +01:00
Ozzie Isaacs 6889456662 Merge remote-tracking branch 'adv_search/master' into master 2021-03-20 17:48:21 +01:00
Ozzie Isaacs 081553dc9f Disable blank issue templates 2021-03-20 16:27:47 +01:00
Ozzie Isaacs 5d8d796807 Compatibility for sqlalchemy 2.0 2021-03-20 10:09:08 +01:00
Ozzie Isaacs f3d88fc746 Update sqlalchemy 1.4 working 2021-03-20 09:31:29 +01:00
Ozzie Isaacs d87ccae6c9 Merge branch 'master' into development
# Conflicts:
#	cps/server.py
#	test/Calibre-Web TestSummary_Linux.html
2021-03-19 20:33:38 +01:00
Ozzie Isaacs 2760a7816d Fix metadata recognition fb2 files 2021-03-19 20:32:15 +01:00
Ozzie Isaacs 8f5c649d0f Updated dependency comicapi (removed pypdf2)
Updated dependency pyPDF2 ->pyPDF3
Fixed broken updater in case of http error
Bugfixes from testrun
Bugfix load cover for BasicMetadata
2021-03-19 13:35:52 +01:00
Ozzie Isaacs fcf9e7a1ef Upload pdf fixes:
Handle no title
Handle no author
Fix import of more than one language
Add missing pdf upload publisher handling
2021-03-17 19:06:51 +01:00
Ozzie Isaacs 2be7b6480a Merge remote-tracking branch 'pdf/XMP_Metadata3' into master 2021-03-17 19:05:26 +01:00
cbartondock f5ded86c02 removed extraneous logs 2021-03-16 19:18:57 -04:00
rra 8abfaf0ffd Parse XMP metadata in separate function, add exception, try multiple metadata formats 2021-03-16 17:53:33 +01:00
Ozzie Isaacs b070ba142f Selects are working in user management with generic confirm dialog 2021-03-16 15:52:31 +01:00
Ozzie Isaacs bd7c6828bf Selects are working in user management 2021-03-16 15:03:38 +01:00
cbartondock d0671ec58c Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-03-15 21:09:51 -04:00
cbartondock 1e40ffd1cc merge changes 2021-03-15 21:08:41 -04:00
Ozzie Isaacs da2c3e9ed7 Edit user mass change checkbox working 2021-03-15 16:46:27 +01:00
Ozzie Isaacs f62d6abb69 Merge branch 'master' into development
# Conflicts:
#	cps/server.py
#	test/Calibre-Web TestSummary_Linux.html
2021-03-15 13:53:23 +01:00
Ozzie Isaacs 12ad7a6322 Suppress some errors 2021-03-15 13:52:44 +01:00
Ozzie Isaacs b75247ea3a Suppress some errors 2021-03-15 13:48:05 +01:00
Ozzie Isaacs 9a963bbe79 Refactored code
Testrun
2021-03-15 13:10:17 +01:00
Ozzie Isaacs 994bc8b0e4 Removed dropdown button on single file listen/read (Fix #1840) 2021-03-15 09:06:40 +01:00
Ozzie Isaacs 2451605033 Merge remote-tracking branch 'djvu/read-djvu' into master 2021-03-15 08:43:25 +01:00
Ozzie Isaacs 10942527f3 Merge branch 'master' into development
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2021-03-15 08:22:12 +01:00
Ozzie Isaacs 4909ed5ccd Merge remote-tracking branch 'cliblur/master' into development
# Conflicts:
#	cps/static/css/style.css
2021-03-15 08:21:44 +01:00
Ozzie Isaacs 5cf5df68dc Merge remote-tracking branch 'cliblur/master' into master 2021-03-15 08:19:56 +01:00
Ozzie Isaacs dd32cc99ea Merge remote-tracking branch 'es/patch-1' into master 2021-03-15 08:17:14 +01:00
Ozzie Isaacs 79092dc8eb Merge remote-tracking branch 'it/patch-21' into master 2021-03-15 08:11:41 +01:00
Ozzie Isaacs 6229e4610a Updated teststatus and bugfix convert ebooks 2021-03-15 08:10:42 +01:00
Northguy bfe36d3f4a change in color definition
Making color the same as in:
.container-fluid .book .meta .author, .container-fluid .book .meta .author > a {
    color: hsla(0, 0%, 100%, .45)
}
2021-03-15 00:12:02 +01:00
Northguy d42bf44fad Added same color for series name and series number 2021-03-14 23:46:33 +01:00
rra 33e352819c Merge remote-tracking branch 'upstream/master' into XMP_Metadata3 2021-03-14 19:58:26 +01:00
Ozzie Isaacs 53ee0aaee1 Some functions refactored 2021-03-14 17:34:47 +01:00
Ozzie Isaacs 42707a19bd Code cosmetics 2021-03-14 16:57:33 +01:00
Ozzie Isaacs 0888706790 Code cosmetics 2021-03-14 16:53:01 +01:00
Ozzie Isaacs 16453a05f8 Code cosmetics 2021-03-14 16:02:37 +01:00
Ozzie Isaacs 2fbb7466d3 Code cosmetics 2021-03-14 15:44:40 +01:00
Ozzie Isaacs f29f94f45f Code cosmetics 2021-03-14 15:41:36 +01:00
Ozzie Isaacs cd973868fc Code cosmetics 2021-03-14 15:29:34 +01:00
Ozzie Isaacs 3c35f02cac Code cosmetics 2021-03-14 15:18:45 +01:00
Ozzie Isaacs 22c93e2389 Merge branch 'master' into development 2021-03-14 15:06:20 +01:00
Ozzie Isaacs 8c751eb532 Some code cosmetics 2021-03-14 15:06:09 +01:00
Ozzie Isaacs 4df443e007 Some code cosmetics 2021-03-14 14:40:04 +01:00
Ozzie Isaacs f52fa41439 Fix restart server
Some code cosmetics
2021-03-14 14:29:40 +01:00
Ozzie Isaacs f77d72fd86 Merge branch 'master' into development
# Conflicts:
#	cps/static/css/style.css
2021-03-14 14:06:33 +01:00
Ozzie Isaacs 9b80c84794 Some code cosmetics 2021-03-14 14:05:36 +01:00
Ozzie Isaacs 725fc658f8 Some code cosmetics 2021-03-14 13:28:52 +01:00
alfred82santa 24bbf226a1 Sync reading state only for books on kobo shelves 2021-03-14 11:29:23 +01:00
Ozzie Isaacs e4e27662f5 Merge remote-tracking branch 'url/patch-1' into master 2021-03-14 08:35:37 +01:00
cbartondock 83474da7b5 fixed being able to click through dropdown in advanced search results 2021-03-07 22:54:34 -05:00
cbartondock 9146e5f287 fixed advanced search mass add to shelves in caliblur 2021-03-07 19:53:54 -05:00
cbartondock ff4502c63a Made shelf search user specific 2021-03-07 15:23:05 -05:00
cbartondock 9711bd8fe1 added actions boxes to drop downs 2021-03-06 22:34:39 -05:00
cbartondock 05139e53be advanced shelf search is working 2021-03-06 22:15:18 -05:00
cbartondock 870b2642a9 advanced shelf search is almost working 2021-03-06 21:54:39 -05:00
ElQuimm d31b26ae7d Updated italian message.po
Have a nice day.
2021-03-04 14:26:36 +01:00
Zaroz 5511925ba2 Removed elif "url" identifier format type
Removed elif "url" identifier format type, since else output is identical
2021-03-03 21:18:08 -06:00
Angel Docampo f96b20717d Updated spanish messages.po
Corrected a grammatical error
2021-03-04 00:01:03 +01:00
Zaroz 940c9c45d7 Updated db.py
changed __repr__ else statement to allow for custom id entries
2021-03-02 23:46:02 -06:00
Ozzie Isaacs 87d6008dfc Fix for #1870 (png was missing in supported picture formats for comic viewer) 2021-02-27 09:23:34 +01:00
Ozzie Isaacs b9c0c8d2dc Update bootstrap table to 1.18.2 2021-02-20 20:03:39 +01:00
Ozzie Isaacs 81c30d5fd5 Add missing files from bootstrap editable
Added user-table single-select header for locale and default-language
2021-02-20 19:55:51 +01:00
Ozzie Isaacs 0aa33d88a5 Merge branch 'master' into development 2021-02-20 14:19:39 +01:00
Ozzie Isaacs e64a504bb1 Working Locale and default language selection in user table edit 2021-02-20 14:18:27 +01:00
jvoisin bc876a159e Declare variables before using them
It should fix the following stacktrace:

```
[2021-02-18 14:46:14,771] ERROR {cps:1891} Exception on / [GET]
Traceback (most recent call last):
  File "/opt/calibre/vendor/flask/app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "/opt/calibre/vendor/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/opt/calibre/vendor/flask/app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/opt/calibre/vendor/flask/_compat.py", line 39, in reraise
    raise value
  File "/opt/calibre/vendor/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/opt/calibre/vendor/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/opt/calibre/cps/usermanagement.py", line 38, in decorated_view
    return login_required(func)(*args, **kwargs)
  File "/opt/calibre/vendor/flask_login/utils.py", line 272, in decorated_view
    return func(*args, **kwargs)
  File "/opt/calibre/cps/web.py", line 719, in index
    return render_books_list("newest", sort_param, 1, page)
  File "/opt/calibre/cps/web.py", line 422, in render_books_list
    entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order)
  File "/opt/calibre/cps/db.py", line 610, in fill_indexpage
    return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
  File "/opt/calibre/cps/db.py", line 635, in fill_indexpage_with_archived_books
    #    book = self.order_authors(book)
UnboundLocalError: local variable 'entries' referenced before assignment
```
2021-02-18 17:02:58 +01:00
Ozzieisaacs 4aa1a838ed Removed f-strings, making it compatible with python 3.5 2021-02-15 14:17:05 +01:00
Ozzieisaacs 095a51edd0 Convert tabs to spaces in server.py (related to #1862) 2021-02-15 13:33:42 +01:00
Ozzie Isaacs a3a11bdf3f Changed checkbox states are saved on server 2021-02-14 17:49:40 +01:00
Ozzie Isaacs 70b503f3d4 Merge branch 'master' into development
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2021-02-13 14:36:42 +01:00
Ozzie Isaacs bbf609b880 Fix #1859 (Upload of jpg files without installed imagemagick doesn't work) 2021-02-13 14:34:41 +01:00
Ozzie Isaacs 0992bafe30 Bulk User management 2021-02-13 13:17:02 +01:00
Ozzie Isaacs c810c5275a Merge branch 'master' into development 2021-02-09 18:05:32 +01:00
Ozzie Isaacs 3c1b06872d Updated testresults
Changed optional-requirements
Prevent redirect 308 on /<data> routes (#1854)
2021-02-09 18:04:56 +01:00
Ozzie Isaacs cefdd2f66c suppress asyncio message always on restart or shutdown 2021-02-08 20:39:21 +01:00
Ozzie Isaacs 5dac13b1da Revert change in ub.py 2021-02-08 20:17:02 +01:00
alfred82santa 6014b04b2a Use BookShelf added_date as date reference 2021-02-08 20:10:12 +01:00
Ozzie Isaacs 8aebf48193 Update handling for package data 2021-02-08 20:09:36 +01:00
Ozzie Isaacs fbb905957b Prevent redirect 308 on /<data> routes 2021-02-08 20:09:36 +01:00
Ozzie Isaacs e0ce135838 Move "create_engine" call 2021-02-08 20:09:36 +01:00
Ozzie Isaacs 60497c60c1 Update creating Home_Config variable for packaging support, added .HOMEDIR to ignored filename during update 2021-02-07 09:20:37 +01:00
Ozzie Isaacs 251a77c8b4 Merge branch 'master' into Development
Improved packaging support
2021-02-07 08:54:29 +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
Ozzie Isaacs 7b7494b8a4 Added executable folder to gitignore 2021-02-06 21:42:29 +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
Ozzie Isaacs 9390dcdd43 Update optional requirements 2021-02-06 11:33:20 +01:00
Ozzie Isaacs e6fb460071 New restart routine for executable files
Fix log in updater
New source option updater available
2021-02-06 09:42:27 +01:00
Ozzie Isaacs 6137fdeb33 Version bump 2021-02-02 19:11:02 +01:00
Ozzie Isaacs 4a4d02ea6a Added translations 2021-02-02 19:04:55 +01:00
Ozzie Isaacs be26e5f152 Update Teststatus
Update Version
2021-02-02 18:52:31 +01:00
Ozzie Isaacs 127bfba135 fix wrong path in get_update_status js function 2021-02-01 19:00:10 +01:00
Ozzie Isaacs 7efae3c125 Renamed "Recently Added" to "Books" 2021-01-31 19:20:43 +01:00
Ozzie Isaacs 1e5af21000 Fix #1843 (get path to script via html file and known js file instead of randomly picking last one) 2021-01-31 19:01:34 +01:00
Ozzie Isaacs 33a0a4c173 Changed function for getting path in js file 2021-01-31 18:55:32 +01:00
Ozzie Isaacs eeb7974e05 User table:
Added button for single user edit
Added delete-trash-icon (not working)
Roles are displayed correct per user
Guest user is not visible if anonymous browsing is disabled
2021-01-31 14:54:45 +01:00
Ozzie Isaacs f45ea1a31c Merge branch 'master' into development 2021-01-31 13:21:47 +01:00
Ozzie Isaacs a866dbaa80 Fix mimetype in comic reader 2021-01-31 11:21:09 +01:00
Ozzie Isaacs 62447d6b89 Basic User edit in tables 2021-01-31 10:17:57 +01:00
Ozzie Isaacs 93f0724b83 Merge branch 'development' into master 2021-01-31 09:11:35 +01:00
Ozzie Isaacs 724762843d Update Teststatus 2021-01-31 09:10:42 +01:00
Ozzie Isaacs ff16afbf0b Merge branch 'master' into development 2021-01-30 17:55:45 +01:00
Ozzie Isaacs 9d7ef25062 Fix Kobo sync & Update Teststatus 2021-01-30 17:55:17 +01:00
Ozzie Isaacs 88078d65e9 Fix #1482 (do not shorten title in book detail page) 2021-01-30 14:57:59 +01:00
Ozzie Isaacs b07a97c17e Merge remote-tracking branch 'webp/comic-webp' into master 2021-01-30 14:56:26 +01:00
Ozzie Isaacs 41e7d65e2a Merge remote-tracking branch 'caliblur/master' into master 2021-01-30 14:54:16 +01:00
Ozzie Isaacs 7fa5865cf6 Merge remote-tracking branch 'autoconv/kepub_autoconv' into development
# Conflicts:
#	cps/kobo.py
#	cps/kobo_auth.py
2021-01-30 14:47:18 +01:00
Ozzie Isaacs e0b8fe3b1a Merge branch 'master' into development 2021-01-30 14:41:46 +01:00
Ozzie Isaacs 4a11dd1e16 Fix mistyping "option" 2021-01-28 20:20:59 +01:00
Ozzie Isaacs 34a474101f Fix #1838 (Log message on failed OPDS login) 2021-01-28 20:05:58 +01:00
Ozzie Isaacs e6799e7a04 Improved detection of invalid email addresses (#1831) upon registering 2021-01-27 19:18:40 +01:00
Ozzie Isaacs 0f83f9992c Added swipe support for epub reader (fixes #925)
Cleaned reader js include files
2021-01-24 14:19:46 +01:00
Ozzie Isaacs d2ad78eb40 Added swipe support for comic reader and txt reader (#925)
Bugfix for txt file not present on serving file
2021-01-24 11:28:17 +01:00
Ozzie Isaacs 0b32738f4e Fix Store UI settings in flask session for guest user (#1820)
Updated testresults
Fix Filepicker (absolute instead of abs)
2021-01-24 07:31:40 +01:00
Ozzie Isaacs a9cedb3fca Add logger to db.py 2021-01-23 13:35:30 +01:00
Ozzie Isaacs 51f9cd4bb4 Fix english language names 2021-01-18 21:24:13 +01:00
Ozzie Isaacs a1668e2411 Version Bump 2021-01-17 16:37:54 +01:00
mmonkey b4324cd685 Merge remote-tracking branch 'upstream/master' into master 2021-01-04 12:24:11 -06:00
Ozzieisaacs 6fe4ed3e24 Merge branch 'master' into Develop 2021-01-03 19:28:12 +01:00
mmonkey bc8bdfe385 Merge remote-tracking branch 'upstream/master' into master 2021-01-02 20:21:15 -06:00
Ethan Lin ee28e3346b Merge pull request #21 from janeczku/master
merge from janeczku/master
2020-12-29 10:31:41 +08:00
mmonkey 0cf4c7b7b7 Merge remote-tracking branch 'upstream/master' into master 2020-12-24 03:01:55 -06:00
andylizi 123493ee59 Fix #1255 (support webp in comic viewer) 2020-12-17 23:54:06 +08:00
mmonkey 2d498dd138 Preserve publisher links, and fix formatting in custom columns for CaliBlur theme. 2020-12-16 00:58:45 -06:00
verglor 2343c79126 Use djvu_html5 to read djvu format in browser 2020-11-15 13:19:37 +01:00
verglor 09a5a69f86 djvu_html5 0.3.0-beta1 static assets 2020-11-15 13:09:32 +01:00
dickreckard 65929c02bc isolanguage parsing of xmp data 2020-09-11 10:49:45 +00:00
root 22466d6b98 xmp data processing added to the uploader 2020-09-11 10:08:55 +00:00
dickreckard 23fe79c618 Update uploader.py 2020-09-11 03:42:19 +02:00
dickreckard cb7727900c Update uploader.py
default cover changed from none to pdf_preview when metadata parsing fails
2020-09-07 13:30:03 +02:00
norangebit bdf6052388 Conversion to kepub when creating a sync token 2020-09-03 21:57:14 +02:00
norangebit 99d653eece Add automatic conversion in kepub
Automatic conversion from epub to kepub of new book on Kobo sync.
2020-09-01 19:27:14 +02:00
839 changed files with 114353 additions and 100537 deletions
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+3 -1
View File
@@ -6,10 +6,12 @@ __pycache__/
# Distribution / packaging
.Python
.python-version
env/
venv/
eggs/
dist/
executable/
build/
vendor/
.eggs/
@@ -29,4 +31,4 @@ vendor/
settings.yaml
gdrive_credentials
client_secrets.json
gmail.json
+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.
+25 -16
View File
@@ -2,6 +2,13 @@
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?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.*
![Main screen](https://github.com/janeczku/calibre-web/wiki/images/main_screen.png)
@@ -12,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 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)
@@ -22,7 +29,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- Support for public user registration
- Send eBooks to Kindle devices with the click of a button
- Sync your Kobo devices through Calibre-Web with your Calibre library
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz)
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
- Support for Calibre Custom Columns
- Ability to hide content based on categories and Custom Column content per user
@@ -32,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\
@@ -48,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:
@@ -58,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)
@@ -80,7 +87,9 @@ Pre-built Docker images are available in these Docker Hub repositories:
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
+ The "path to unrar" should be set to `/usr/bin/unrar`
# Wiki
# Contact
Just reach us out on [Discord](https://discord.gg/h2VsJ2NEfB)
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
+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
+47 -16
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)
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 supporting Python2 please consider upgrading to Python3')
print('Python2 is EOL since end of 2019, this version of Calibre-Web 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,9 +156,8 @@ 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.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale
preferred = list()
@@ -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()
+25 -7
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
@@ -37,6 +40,7 @@ try:
except ImportError:
from flask_login.__about__ import __version__ as flask_loginVersion
try:
# pylint: disable=unused-import
import unidecode
# _() necessary to make babel aware of string for translation
unidecode_version = _(u'installed')
@@ -48,6 +52,17 @@ try:
except ImportError:
flask_danceVersion = None
try:
from greenlet import __version__ as greenlet_Version
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__)
@@ -63,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__,
@@ -72,12 +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
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())
+763 -336
View File
File diff suppressed because it is too large Load Diff
+2 -2
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
@@ -49,7 +48,7 @@ def init_cache_busting(app):
# compute version component
rooted_filename = os.path.join(dirpath, filename)
with open(rooted_filename, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()[:7]
file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec
# save version to tables
file_path = rooted_filename.replace(static_folder, "")
@@ -64,6 +63,7 @@ def init_cache_busting(app):
return filename.split("?", 1)[0]
@app.url_defaults
# pylint: disable=unused-variable
def reverse_to_cache_busted_url(endpoint, values):
"""
Make `url_for` produce busted filenames when using the 'static' endpoint.
+14 -28
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")
@@ -71,7 +57,7 @@ if args.c:
if os.path.isfile(args.c):
certfilepath = args.c
else:
print("Certfilepath is invalid. Exiting...")
print("Certfile path is invalid. Exiting...")
sys.exit(1)
if args.c == "":
@@ -81,7 +67,7 @@ if args.k:
if os.path.isfile(args.k):
keyfilepath = args.k
else:
print("Keyfilepath is invalid. Exiting...")
print("Keyfile path is invalid. Exiting...")
sys.exit(1)
if (args.k and not args.c) or (not args.k and args.c):
@@ -91,29 +77,29 @@ if (args.k and not args.c) or (not args.k and args.c):
if args.k == "":
keyfilepath = ""
# handle and check ipadress argument
ipadress = args.i or None
if ipadress:
# handle and check ip address argument
ip_address = args.i or None
if ip_address:
try:
# try to parse the given ip address with socket
if hasattr(socket, 'inet_pton'):
if ':' in ipadress:
socket.inet_pton(socket.AF_INET6, ipadress)
if ':' in ip_address:
socket.inet_pton(socket.AF_INET6, ip_address)
else:
socket.inet_pton(socket.AF_INET, ipadress)
socket.inet_pton(socket.AF_INET, ip_address)
else:
# on windows python < 3.4, inet_pton is not available
# inet_atom only handles IPv4 addresses
socket.inet_aton(ipadress)
socket.inet_aton(ip_address)
except socket.error as err:
print(ipadress, ':', err)
print(ip_address, ':', err)
sys.exit(1)
# handle and check user password argument
user_credentials = args.s or None
if user_credentials and ":" not in user_credentials:
print("No valid username:password format")
print("No valid 'username:password' format")
sys.exit(3)
# Handles enableing of filepicker
filepicker = args.f or None
if args.f:
print("Warning: -f flag is depreciated and will be removed in next version")
+42 -35
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
@@ -74,6 +73,41 @@ def _cover_processing(tmp_file_name, img, extension):
return tmp_cover_name
def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable):
cover_data = None
if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name)
for name in cf.namelist():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
elif original_file_extension.upper() == '.CBT':
cf = tarfile.TarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.extractfile(name).read()
break
elif original_file_extension.upper() == '.CBR' and use_rarfile:
try:
rarfile.UNRAR_TOOL = rarExecutable
cf = rarfile.RarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
except Exception as ex:
log.debug('Rarfile failed with error: %s', ex)
return cover_data
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
cover_data = extension = None
@@ -87,37 +121,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
cover_data = archive.getPage(index)
break
else:
if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name)
for name in cf.namelist():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
elif original_file_extension.upper() == '.CBT':
cf = tarfile.TarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.extractfile(name).read()
break
elif original_file_extension.upper() == '.CBR' and use_rarfile:
try:
rarfile.UNRAR_TOOL = rarExecutable
cf = rarfile.RarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
except Exception as e:
log.debug('Rarfile failed with error: %s', e)
cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
return _cover_processing(tmp_file_name, cover_data, extension)
@@ -142,13 +146,15 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
file_path=tmp_file_path,
extension=original_file_extension,
title=loadedMetadata.title or original_file_name,
author=" & ".join([credit["person"] for credit in loadedMetadata.credits if credit["role"] == "Writer"]) or u'Unknown',
author=" & ".join([credit["person"]
for credit in loadedMetadata.credits if credit["role"] == "Writer"]) or u'Unknown',
cover=_extractCover(tmp_file_path, original_file_extension, rarExecutable),
description=loadedMetadata.comments or "",
tags="",
series=loadedMetadata.series or "",
series_id=loadedMetadata.issue or "",
languages=loadedMetadata.language)
languages=loadedMetadata.language,
publisher="")
return BookMeta(
file_path=tmp_file_path,
@@ -160,4 +166,5 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
tags="",
series="",
series_id="",
languages="")
languages="",
publisher="")
+53 -36
View File
@@ -16,16 +16,20 @@
# 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
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.expression import text
try:
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub
from . import constants, cli, logger
log = logger.create()
@@ -35,7 +39,7 @@ class _Flask_Settings(_Base):
__tablename__ = 'flask_settings'
id = Column(Integer, primary_key=True)
flask_session_key = Column(BLOB, default="")
flask_session_key = Column(BLOB, default=b"")
def __init__(self, key):
self.flask_session_key = key
@@ -54,19 +58,21 @@ class _Settings(_Base):
mail_password = Column(String, default='mypassword')
mail_from = Column(String, default='automailer <mail@example.com>')
mail_size = Column(Integer, default=25*1024*1024)
mail_server_type = Column(SmallInteger, default=0)
mail_gmail_token = Column(JSON, default={})
config_calibre_dir = Column(String)
config_port = Column(Integer, default=constants.DEFAULT_PORT)
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)
config_authors_max = Column(Integer, default=0)
config_read_column = Column(Integer, default=0)
config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
config_mature_content_tags = Column(String, default='')
config_theme = Column(Integer, default=0)
@@ -83,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="")
@@ -125,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)
@@ -146,15 +155,16 @@ class _ConfigSQL(object):
self.load()
change = False
if self.config_converterpath == None:
if self.config_converterpath == None: # pylint: disable=access-member-before-definition
change = True
self.config_converterpath = autodetect_calibre_binary()
if self.config_kepubifypath == None:
if self.config_kepubifypath == None: # pylint: disable=access-member-before-definition
change = True
self.config_kepubifypath = autodetect_kepubify_binary()
if self.config_rarfile_location == None:
if self.config_rarfile_location == None: # pylint: disable=access-member-before-definition
change = True
self.config_rarfile_location = autodetect_unrar_binary()
if change:
@@ -181,8 +191,9 @@ class _ConfigSQL(object):
return None
return self.config_keyfile
def get_config_ipaddress(self):
return cli.ipadress or ""
@staticmethod
def get_config_ipaddress():
return cli.ip_address or ""
def _has_role(self, role_flag):
return constants.has_flag(self.config_default_role, role_flag)
@@ -240,18 +251,18 @@ class _ConfigSQL(object):
return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')}
def get_mail_server_configured(self):
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER)
return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0)
or (self.mail_gmail_token != {} and self.mail_server_type == 1))
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
'''Possibly updates a field of this object.
"""Possibly updates a field of this object.
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
:returns: `True` if the field has changed value
'''
"""
new_value = dictionary.get(field, default)
if new_value is None:
# log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field)
return False
if field not in self.__dict__:
@@ -268,7 +279,6 @@ class _ConfigSQL(object):
if current_value == new_value:
return False
# log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value)
setattr(self, field, new_value)
return True
@@ -299,7 +309,11 @@ class _ConfigSQL(object):
have_metadata_db = os.path.isfile(db_file)
self.db_configured = have_metadata_db
constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
logfile = logger.setup(self.config_logfile, self.config_log_level)
if os.environ.get('FLASK_DEBUG'):
logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG)
else:
# pylint: disable=access-member-before-definition
logfile = logger.setup(self.config_logfile, self.config_log_level)
if logfile != self.config_logfile:
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
self.config_logfile = logfile
@@ -334,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()
@@ -345,26 +359,29 @@ def _migrate_table(session, orm_class):
if column_name[0] != '_':
try:
session.query(column).first()
except exc.OperationalError as err:
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:
if isinstance(column.default.arg, bool):
column_default = ("DEFAULT %r" % int(column.default.arg))
column_default = "DEFAULT {}".format(int(column.default.arg))
else:
column_default = ("DEFAULT %r" % column.default.arg)
alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
column_default = "DEFAULT `{}`".format(column.default.arg)
if isinstance(column.type, JSON):
column_type = "JSON"
else:
column_type = column.type
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
column_name,
column.type,
column_default)
column_type,
column_default))
log.debug(alter_table)
session.execute(alter_table)
changed = True
except json.decoder.JSONDecodeError as e:
log.error("Database corrupt column: {}".format(column_name))
log.debug(e)
if changed:
try:
@@ -423,12 +440,12 @@ def load_configuration(session):
session.commit()
conf = _ConfigSQL(session)
# Migrate from global restrictions to user based restrictions
if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
conf.config_denied_tags = conf.config_mature_content_tags
conf.save()
session.query(ub.User).filter(ub.User.mature_content != True). \
update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
session.commit()
#if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
# conf.config_denied_tags = conf.config_mature_content_tags
# conf.save()
# session.query(ub.User).filter(ub.User.mature_content != True). \
# update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
# session.commit()
return conf
def get_flask_session_key(session):
+33 -12
View File
@@ -16,20 +16,21 @@
# 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
HOME_CONFIG = False
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'))
#In executables updater is not available, so variable is set to False there
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')
@@ -40,7 +41,7 @@ if HOME_CONFIG:
os.makedirs(home_dir)
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', home_dir)
else:
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR)
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR)
ROLE_USER = 0 << 0
@@ -84,6 +85,26 @@ SIDEBAR_ARCHIVED = 1 << 15
SIDEBAR_DOWNLOAD = 1 << 16
SIDEBAR_LIST = 1 << 17
sidebar_settings = {
"detail_random": DETAIL_RANDOM,
"sidebar_language": SIDEBAR_LANGUAGE,
"sidebar_series": SIDEBAR_SERIES,
"sidebar_category": SIDEBAR_CATEGORY,
"sidebar_random": SIDEBAR_RANDOM,
"sidebar_author": SIDEBAR_AUTHOR,
"sidebar_best_rated": SIDEBAR_BEST_RATED,
"sidebar_read_and_unread": SIDEBAR_READ_AND_UNREAD,
"sidebar_recent": SIDEBAR_RECENT,
"sidebar_sorted": SIDEBAR_SORTED,
"sidebar_publisher": SIDEBAR_PUBLISHER,
"sidebar_rating": SIDEBAR_RATING,
"sidebar_format": SIDEBAR_FORMAT,
"sidebar_archived": SIDEBAR_ARCHIVED,
"sidebar_download": SIDEBAR_DOWNLOAD,
"sidebar_list": SIDEBAR_LIST,
}
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1
@@ -102,7 +123,7 @@ LDAP_AUTH_SIMPLE = 0
DEFAULT_MAIL_SERVER = "mail.example.org"
DEFAULT_PASSWORD = "admin123"
DEFAULT_PASSWORD = "admin123" # nosec
DEFAULT_PORT = 8083
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
try:
@@ -128,9 +149,9 @@ def selected_roles(dictionary):
# :rtype: BookMeta
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages')
'series_id, languages, publisher')
STABLE_VERSION = {'version': '0.6.10'}
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
+229 -126
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
@@ -30,7 +29,13 @@ from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.exc import OperationalError
try:
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
from sqlalchemy.ext.associationproxy import association_proxy
@@ -38,6 +43,7 @@ from flask_login import current_user
from babel import Locale as LC
from babel.core import UnknownLocaleError
from flask_babel import gettext as _
from flask import flash
from . import logger, ub, isoLanguages
from .pagination import Pagination
@@ -50,7 +56,9 @@ try:
except ImportError:
use_unidecode = False
cc_exceptions = ['datetime', 'comments', 'composite', 'series']
log = logger.create()
cc_exceptions = ['composite', 'series']
cc_classes = {}
Base = declarative_base()
@@ -113,6 +121,8 @@ class Identifiers(Base):
return u"Douban"
elif format_type == "goodreads":
return u"Goodreads"
elif format_type == "babelio":
return u"Babelio"
elif format_type == "google":
return u"Google Books"
elif format_type == "kobo":
@@ -140,6 +150,8 @@ class Identifiers(Base):
return u"https://dx.doi.org/{0}".format(self.val)
elif format_type == "goodreads":
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
elif format_type == "babelio":
return u"https://www.babelio.com/livres/titre/{0}".format(self.val)
elif format_type == "douban":
return u"https://book.douban.com/subject/{0}".format(self.val)
elif format_type == "google":
@@ -154,10 +166,8 @@ class Identifiers(Base):
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
elif format_type == "isfdb":
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
elif format_type == "url":
return u"{0}".format(self.val)
else:
return u""
return u"{0}".format(self.val)
class Comments(Base):
@@ -326,7 +336,6 @@ class Books(Base):
has_cover = Column(Integer, default=0)
uuid = Column(String)
isbn = Column(String(collation='NOCASE'), default="")
# Iccn = Column(String(collation='NOCASE'), default="")
flags = Column(Integer, nullable=False, default=1)
authors = relationship('Authors', secondary=books_authors_link, backref='books')
@@ -377,28 +386,29 @@ 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
class AlchemyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj.__class__, DeclarativeMeta):
def default(self, o):
if isinstance(o.__class__, DeclarativeMeta):
# an SQLAlchemy class
fields = {}
for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x != "password"]:
if field == 'books':
continue
data = obj.__getattribute__(field)
data = o.__getattribute__(field)
try:
if isinstance(data, str):
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))
@@ -411,12 +421,12 @@ class AlchemyEncoder(json.JSONEncoder):
else:
json.dumps(data)
fields[field] = data
except:
except Exception:
fields[field] = ""
# a json-encodable dict
return fields
return json.JSONEncoder.default(self, obj)
return json.JSONEncoder.default(self, o)
class CalibreDB():
@@ -437,26 +447,121 @@ class CalibreDB():
self.instances.add(self)
def initSession(self, expire_on_commit=True):
self.session = self.session_factory()
self.session.expire_on_commit = expire_on_commit
self.update_title_sort(self.config)
@classmethod
def setup_db(cls, config, app_db_path):
def setup_db_cc_classes(self, cc):
cc_ids = []
books_custom_column_links = {}
for row in cc:
if row.datatype not in cc_exceptions:
if row.datatype == 'series':
dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link',
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id'),
primary_key=True),
'map_value': Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True),
'extra': Column(Float),
'asoc': relationship('custom_column_' + str(row.id), uselist=False),
'value': association_proxy('asoc', 'value')
}
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
(Base,), dicttable)
if row.datatype in ['rating', 'text', 'enumeration']:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
Base.metadata,
Column('book', Integer, ForeignKey('books.id'),
primary_key=True),
Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True)
)
cc_ids.append([row.id, row.datatype])
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True)}
if row.datatype == 'float':
ccdict['value'] = Column(Float)
elif row.datatype == 'int':
ccdict['value'] = Column(Integer)
elif row.datatype == 'datetime':
ccdict['value'] = Column(TIMESTAMP)
elif row.datatype == 'bool':
ccdict['value'] = Column(Boolean)
else:
ccdict['value'] = Column(String)
if row.datatype in ['float', 'int', 'bool', 'datetime', 'comments']:
ccdict['book'] = Column(Integer, ForeignKey('books.id'))
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
for cc_id in cc_ids:
if cc_id[1] in ['bool', 'int', 'float', 'datetime', 'comments']:
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]],
primaryjoin=(
Books.id == cc_classes[cc_id[0]].book),
backref='books'))
elif cc_id[1] == 'series':
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(books_custom_column_links[cc_id[0]],
backref='books'))
else:
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]],
secondary=books_custom_column_links[cc_id[0]],
backref='books'))
return cc_classes
@classmethod
def check_valid_db(cls, config_calibre_dir, app_db_path):
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:
@@ -465,84 +570,24 @@ class CalibreDB():
isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False},
poolclass=StaticPool)
cls.engine.execute("attach database '{}' as calibre;".format(dbpath))
cls.engine.execute("attach database '{}' as app_settings;".format(app_db_path))
with cls.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)))
conn = cls.engine.connect()
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
except Exception as e:
config.invalidate(e)
except Exception as ex:
cls.config.invalidate(ex)
return False
config.db_configured = True
cls.config.db_configured = True
if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns")
cc_ids = []
books_custom_column_links = {}
for row in cc:
if row.datatype not in cc_exceptions:
if row.datatype == 'series':
dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link',
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id'),
primary_key=True),
'map_value': Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True),
'extra': Column(Float),
'asoc': relationship('custom_column_' + str(row.id), uselist=False),
'value': association_proxy('asoc', 'value')
}
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
(Base,), dicttable)
else:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
Base.metadata,
Column('book', Integer, ForeignKey('books.id'),
primary_key=True),
Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True)
)
cc_ids.append([row.id, row.datatype])
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True)}
if row.datatype == 'float':
ccdict['value'] = Column(Float)
elif row.datatype == 'int':
ccdict['value'] = Column(Integer)
elif row.datatype == 'bool':
ccdict['value'] = Column(Boolean)
else:
ccdict['value'] = Column(String)
if row.datatype in ['float', 'int', 'bool']:
ccdict['book'] = Column(Integer, ForeignKey('books.id'))
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
for cc_id in cc_ids:
if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'):
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]],
primaryjoin=(
Books.id == cc_classes[cc_id[0]].book),
backref='books'))
elif (cc_id[1] == 'series'):
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(books_custom_column_links[cc_id[0]],
backref='books'))
else:
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]],
secondary=books_custom_column_links[cc_id[0]],
backref='books'))
try:
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)
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
autoflush=True,
@@ -563,8 +608,8 @@ class CalibreDB():
def get_book_by_uuid(self, book_uuid):
return self.session.query(Books).filter(Books.uuid == book_uuid).first()
def get_book_format(self, book_id, format):
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == format).first()
def get_book_format(self, book_id, file_format):
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
# Language and content filters for displaying in the UI
def common_filters(self, allow_show_archived=False):
@@ -589,20 +634,46 @@ class CalibreDB():
neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list))
pos_content_tags_filter = true() if postags_list == [''] else Books.tags.any(Tags.name.in_(postags_list))
if self.config.config_restricted_column:
pos_cc_list = current_user.allowed_column_value.split(',')
pos_content_cc_filter = true() if pos_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list))
neg_cc_list = current_user.denied_column_value.split(',')
neg_content_cc_filter = false() if neg_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
try:
pos_cc_list = current_user.allowed_column_value.split(',')
pos_content_cc_filter = true() if pos_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list))
neg_cc_list = current_user.denied_column_value.split(',')
neg_content_cc_filter = false() if neg_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
except (KeyError, AttributeError):
pos_content_cc_filter = false()
neg_content_cc_filter = true()
log.error(u"Custom Column No.%d is not existing in calibre database",
self.config.config_restricted_column)
flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=self.config.config_restricted_column),
category="error")
else:
pos_content_cc_filter = true()
neg_content_cc_filter = false()
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
@staticmethod
def get_checkbox_sorted(inputlist, state, offset, limit, order):
outcome = list()
elementlist = {ele.id: ele for ele in inputlist}
for entry in state:
try:
outcome.append(elementlist[entry])
except KeyError:
pass
del elementlist[entry]
for entry in elementlist:
outcome.append(elementlist[entry])
if order == "asc":
outcome.reverse()
return outcome[offset:offset + limit]
# Fill indexpage with all requested data from database
def fill_indexpage(self, page, pagesize, database, db_filter, order, *join):
return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
@@ -614,22 +685,35 @@ class CalibreDB():
randm = self.session.query(Books) \
.filter(self.common_filters(allow_show_archived)) \
.order_by(func.random()) \
.limit(self.config.config_random_books)
.limit(self.config.config_random_books).all()
else:
randm = false()
off = int(int(pagesize) * (page - 1))
query = self.session.query(database) \
.join(*join, isouter=True) \
.filter(db_filter) \
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:
query = query.outerjoin(join[0], join[1])
elif len(join) == 1:
query = query.outerjoin(join[0])
query = query.filter(db_filter)\
.filter(self.common_filters(allow_show_archived))
entries = list()
pagination = list()
try:
pagination = Pagination(page, pagesize,
len(query.all()))
entries = query.order_by(*order).offset(off).limit(pagesize).all()
except Exception as e:
logger.debug(e)
for book in entries:
book = self.order_authors(book)
except Exception as ex:
log.debug_or_exception(ex)
#for book in entries:
# book = self.order_authors(book)
return entries, randm, pagination
# Orders all Authors in the list according to authors sort
@@ -656,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
@@ -669,23 +754,35 @@ class CalibreDB():
return self.session.query(Books) \
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
# read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(self, term, offset=None, order=None, limit=None):
order = order or [Books.sort]
pagination = None
def search_query(self, term, *join):
term.strip().lower()
self.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list()
authorterms = re.split("[, ]+", term)
for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
result = self.session.query(Books).filter(self.common_filters(True)).filter(
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:
query = query.outerjoin(join[0], join[1])
elif len(join) == 1:
query = query.outerjoin(join[0])
return query.filter(self.common_filters(True)).filter(
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
Books.authors.any(and_(*q)),
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
func.lower(Books.title).ilike("%" + term + "%")
)).order_by(*order).all()
))
# read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(self, term, offset=None, order=None, limit=None, *join):
order = order or [Books.sort]
pagination = None
result = self.search_query(term, *join).order_by(*order).all()
result_count = len(result)
if offset != None and limit != None:
offset = int(offset)
@@ -709,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):
@@ -740,7 +842,7 @@ class CalibreDB():
if old_session:
try:
old_session.close()
except:
except Exception:
pass
if old_session.bind:
try:
@@ -765,13 +867,14 @@ 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):
try:
return unidecode.unidecode(s.lower())
except Exception as e:
except Exception as ex:
log = logger.create()
log.debug_or_exception(e)
log.debug_or_exception(ex)
return s.lower()
+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")
+601 -381
View File
File diff suppressed because it is too large Load Diff
+33 -25
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
@@ -87,18 +86,29 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
lang = epub_metadata['language'].split('-', 1)[0].lower()
epub_metadata['language'] = isoLanguages.get_lang3(lang)
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
if len(series) > 0:
epub_metadata['series'] = series[0]
else:
epub_metadata['series'] = ''
epub_metadata = parse_epbub_series(ns, tree, epub_metadata)
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
if len(series_id) > 0:
epub_metadata['series_id'] = series_id[0]
else:
epub_metadata['series_id'] = '1'
coverfile = parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path)
if not epub_metadata['title']:
title = original_file_name
else:
title = epub_metadata['title']
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title.encode('utf-8').decode('utf-8'),
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
cover=coverfile,
description=epub_metadata['description'],
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
languages=epub_metadata['language'],
publisher="")
def parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path):
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
coverfile = None
if len(coversection) > 0:
@@ -126,20 +136,18 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
coverfile = extractCover(epubZip, filename, "", tmp_file_path)
else:
coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path)
return coverfile
if not epub_metadata['title']:
title = original_file_name
def parse_epbub_series(ns, tree, epub_metadata):
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
if len(series) > 0:
epub_metadata['series'] = series[0]
else:
title = epub_metadata['title']
epub_metadata['series'] = ''
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title.encode('utf-8').decode('utf-8'),
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
cover=coverfile,
description=epub_metadata['description'],
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
languages=epub_metadata['language'])
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
if len(series_id) > 0:
epub_metadata['series_id'] = series_id[0]
else:
epub_metadata['series_id'] = '1'
return epub_metadata
+3 -7
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
@@ -60,14 +62,8 @@ def init_errorhandler():
if services.ldap:
# Only way of catching the LDAPException upon logging in with LDAP server down
@app.errorhandler(services.ldap.LDAPException)
# pylint: disable=unused-variable
def handle_exception(e):
log.debug('LDAP server not accessible while trying to login to opds feed')
return error_http(FailedDependency())
# @app.errorhandler(InvalidRequestError)
#@app.errorhandler(OperationalError)
#def handle_db_exception(e):
# db.session.rollback()
# log.error('Database request error: %s',e)
# return internal_error(InternalServerError(e))
+15 -15
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from lxml import etree
from .constants import BookMeta
@@ -29,52 +28,53 @@ def get_fb2_info(tmp_file_path, original_file_extension):
'l': 'http://www.w3.org/1999/xlink',
}
fb2_file = open(tmp_file_path)
tree = etree.fromstring(fb2_file.read())
fb2_file = open(tmp_file_path, encoding="utf-8")
tree = etree.fromstring(fb2_file.read().encode())
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
def get_author(element):
last_name = element.xpath('fb:last-name/text()', namespaces=ns)
if len(last_name):
last_name = last_name[0].encode('utf-8')
last_name = last_name[0]
else:
last_name = u''
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
if len(middle_name):
middle_name = middle_name[0].encode('utf-8')
middle_name = middle_name[0]
else:
middle_name = u''
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
if len(first_name):
first_name = first_name[0].encode('utf-8')
first_name = first_name[0]
else:
first_name = u''
return (first_name.decode('utf-8') + u' '
+ middle_name.decode('utf-8') + u' '
+ last_name.decode('utf-8')).encode('utf-8')
return (first_name + u' '
+ middle_name + u' '
+ last_name)
author = str(", ".join(map(get_author, authors)))
title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
if len(title):
title = str(title[0].encode('utf-8'))
title = str(title[0])
else:
title = u''
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
if len(description):
description = str(description[0].encode('utf-8'))
description = str(description[0])
else:
description = u''
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title.decode('utf-8'),
author=author.decode('utf-8'),
title=title,
author=author,
cover=None,
description=description.decode('utf-8'),
description=description,
tags="",
series="",
series_id="",
languages="")
languages="",
publisher="")
+42 -44
View File
@@ -20,9 +20,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os
import sys
import hashlib
import json
import tempfile
@@ -34,7 +32,7 @@ from flask import Blueprint, flash, request, redirect, url_for, abort
from flask_babel import gettext as _
from flask_login import login_required
from . import logger, gdriveutils, config, ub, calibre_db
from . import logger, gdriveutils, config, ub, calibre_db, csrf
from .admin import admin_required
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
@@ -47,7 +45,7 @@ except ImportError as err:
current_milli_time = lambda: int(round(time() * 1000))
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
gdrive_watch_callback_token = 'target=calibreweb-watch_files' #nosec
@gdrive.route("/authenticate")
@@ -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):
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 e:
log.debug_or_exception(e)
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
+74 -44
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
@@ -28,8 +27,13 @@ from sqlalchemy import create_engine
from sqlalchemy import Column, UniqueConstraint
from sqlalchemy import String, Integer
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
try:
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
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
@@ -164,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]:
@@ -198,8 +202,8 @@ def getDrive(drive=None, gauth=None):
gauth.Refresh()
except RefreshError as e:
log.error("Google Drive error: %s", e)
except Exception as e:
log.debug_or_exception(e)
except Exception as ex:
log.debug_or_exception(ex)
else:
# Initialize the saved creds
gauth.Authorize()
@@ -217,7 +221,7 @@ def listRootFolders():
drive = getDrive(Gdrive.Instance().drive)
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
fileList = drive.ListFile({'q': folder}).GetList()
except (ServerNotFoundError, ssl.SSLError) as e:
except (ServerNotFoundError, ssl.SSLError, RefreshError) as e:
log.info("GDrive Error %s" % e)
fileList = []
return fileList
@@ -253,7 +257,12 @@ def getEbooksFolderId(drive=None):
log.error('Error gDrive, root ID not found')
gDriveId.path = '/'
session.merge(gDriveId)
session.commit()
try:
session.commit()
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
return gDriveId.gdrive_id
@@ -268,37 +277,42 @@ def getFile(pathId, fileName, drive):
def getFolderId(path, drive):
# drive = getDrive(drive)
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
try:
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
if not storedPathName:
dbChange = False
s = path.split('/')
for i, x in enumerate(s):
if len(x) > 0:
currentPath = "/".join(s[:i+1])
if currentPath[-1] != '/':
currentPath = currentPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first()
if storedPathName:
currentFolderId = storedPathName.gdrive_id
else:
currentFolder = getFolderInFolder(currentFolderId, x, drive)
if currentFolder:
gDriveId = GdriveId()
gDriveId.gdrive_id = currentFolder['id']
gDriveId.path = currentPath
session.merge(gDriveId)
dbChange = True
currentFolderId = currentFolder['id']
if not storedPathName:
dbChange = False
s = path.split('/')
for i, x in enumerate(s):
if len(x) > 0:
currentPath = "/".join(s[:i+1])
if currentPath[-1] != '/':
currentPath = currentPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first()
if storedPathName:
currentFolderId = storedPathName.gdrive_id
else:
currentFolderId = None
break
if dbChange:
session.commit()
else:
currentFolderId = storedPathName.gdrive_id
currentFolder = getFolderInFolder(currentFolderId, x, drive)
if currentFolder:
gDriveId = GdriveId()
gDriveId.gdrive_id = currentFolder['id']
gDriveId.path = currentPath
session.merge(gDriveId)
dbChange = True
currentFolderId = currentFolder['id']
else:
currentFolderId = None
break
if dbChange:
session.commit()
else:
currentFolderId = storedPathName.gdrive_id
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
return currentFolderId
@@ -342,7 +356,7 @@ def moveGdriveFolderRemote(origin_file, target_folder):
addParents=gFileTargetDir['id'],
removeParents=previous_parents,
fields='id, parents').execute()
# if previous_parents has no childs anymore, delete original fileparent
# if previous_parents has no children anymore, delete original fileparent
if len(children['items']) == 1:
deleteDatabaseEntry(previous_parents)
drive.auth.service.files().delete(fileId=previous_parents).execute()
@@ -493,8 +507,8 @@ def getChangeById (drive, change_id):
except (errors.HttpError) as error:
log.error(error)
return None
except Exception as e:
log.error(e)
except Exception as ex:
log.error(ex)
return None
@@ -503,9 +517,10 @@ def deleteDatabaseOnChange():
try:
session.query(GdriveId).delete()
session.commit()
except (OperationalError, InvalidRequestError):
except (OperationalError, InvalidRequestError) as ex:
session.rollback()
log.info(u"GDrive DB is not Writeable")
log.debug('Database error: %s', ex)
log.error(u"GDrive DB is not Writeable")
def updateGdriveCalibreFromLocal():
@@ -520,13 +535,23 @@ def updateDatabaseOnEdit(ID,newPath):
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
if storedPathName:
storedPathName.path = sqlCheckPath
session.commit()
try:
session.commit()
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
# Deletes the hashes in database of deleted book
def deleteDatabaseEntry(ID):
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
session.commit()
try:
session.commit()
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
# Gets cover file from gdrive
@@ -543,7 +568,12 @@ def get_cover_via_gdrive(cover_path):
permissionAdded = PermissionAdded()
permissionAdded.gdrive_id = df['id']
session.add(permissionAdded)
session.commit()
try:
session.commit()
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
return df.metadata.get('webContentLink')
else:
return None
+126 -110
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
@@ -35,9 +34,10 @@ from babel.units import format_unit
from flask import send_from_directory, make_response, redirect, abort, url_for
from flask_babel import gettext as _
from flask_login import current_user
from sqlalchemy.sql.expression import true, false, and_, text
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
@@ -68,6 +68,7 @@ try:
except (ImportError, RuntimeError) as e:
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
use_IM = False
MissingDelegateError = BaseException
# Convert existing book entry to new format
@@ -96,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))
@@ -133,62 +135,50 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
text=txt
))
return
def check_send_to_kindle_with_converter(formats):
bookformats = list()
if 'EPUB' in formats and 'MOBI' not in formats:
bookformats.append({'format': 'Mobi',
'convert': 1,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Epub',
format='Mobi')})
if 'AZW3' in formats and not 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Azw3',
format='Mobi')})
return bookformats
def check_send_to_kindle(entry):
"""
returns all available book formats for sending to Kindle
"""
formats = list()
bookformats = list()
if len(entry.data):
bookformats = list()
if not config.config_converterpath:
# no converter - only for mobi and pdf formats
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
if 'MOBI' in ele.format:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'PDF' in ele.format:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if 'AZW' in ele.format:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
else:
formats = list()
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
formats.append(ele.format)
if 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'AZW' in formats:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
if 'PDF' in formats:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if config.config_converterpath:
if 'EPUB' in formats and 'MOBI' not in formats:
bookformats.append({'format': 'Mobi',
'convert':1,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Epub',
format='Mobi')})
if 'AZW3' in formats and not 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Azw3',
format='Mobi')})
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
formats.append(ele.format)
if 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'PDF' in formats:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if 'AZW' in formats:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
if config.config_converterpath:
bookformats.extend(check_send_to_kindle_with_converter(formats))
return bookformats
else:
log.error(u'Cannot find book entry %d', entry.id)
@@ -198,7 +188,7 @@ def check_send_to_kindle(entry):
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
# list with supported formats
def check_read_formats(entry):
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR'}
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'}
bookformats = list()
if len(entry.data):
for ele in iter(entry.data):
@@ -225,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?")
@@ -241,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)
@@ -259,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):
@@ -340,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
@@ -393,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",
@@ -408,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",
@@ -491,8 +479,8 @@ def reset_password(user_id):
password = generate_random_password()
existing_user.password = generate_password_hash(password)
ub.session.commit()
send_registration_mail(existing_user.email, existing_user.nickname, password, True)
return 1, existing_user.nickname
send_registration_mail(existing_user.email, existing_user.name, password, True)
return 1, existing_user.name
except Exception:
ub.session.rollback()
return 0, None
@@ -501,19 +489,42 @@ 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):
output = []
inpt = [ " ".join(inp.split()) for inp in inpt]
for x in inpt:
if x not in output:
output.append(x)
return output
def check_email(email):
email = valid_email(email)
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
log.error(u"Found an existing account for this e-mail address")
raise Exception(_(u"Found an existing account for this e-mail address"))
return email
def check_username(username):
username = username.strip()
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
log.error(u"This username is already taken")
raise Exception (_(u"This username is already taken"))
return username
def valid_email(email):
email = email.strip()
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
email):
log.error(u"Invalid e-mail address format")
raise Exception(_(u"Invalid e-mail address format"))
return email
# ################################# External interface #################################
@@ -561,8 +572,8 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
else:
log.error('%s/cover.jpg not found on Google Drive', book.path)
return get_cover_on_failure(use_generic_cover_on_failure)
except Exception as e:
log.debug_or_exception(e)
except Exception as ex:
log.debug_or_exception(ex)
return get_cover_on_failure(use_generic_cover_on_failure)
else:
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
@@ -591,22 +602,29 @@ def save_cover_from_url(url, book_path):
def save_cover_from_filestorage(filepath, saved_filename, img):
if hasattr(img,"metadata"):
img.save(filename=os.path.join(filepath, saved_filename))
img.close()
else:
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
if not os.path.exists(filepath):
try:
os.makedirs(filepath)
except OSError:
log.error(u"Failed to create path for cover")
return False, _(u"Failed to create path for cover")
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
if not os.path.exists(filepath):
try:
img.save(os.path.join(filepath, saved_filename))
except (IOError, OSError):
log.error(u"Cover-file is not a valid image file, or could not be stored")
return False, _(u"Cover-file is not a valid image file, or could not be stored")
os.makedirs(filepath)
except OSError:
log.error(u"Failed to create path for cover")
return False, _(u"Failed to create path for cover")
try:
# upload of jgp file without wand
if isinstance(img, requests.Response):
with open(os.path.join(filepath, saved_filename), 'wb') as f:
f.write(img.content)
else:
if hasattr(img, "metadata"):
# upload of jpg/png... via url
img.save(filename=os.path.join(filepath, saved_filename))
img.close()
else:
# upload of jpg/png... from hdd
img.save(os.path.join(filepath, saved_filename))
except (IOError, OSError):
log.error(u"Cover-file is not a valid image file, or could not be stored")
return False, _(u"Cover-file is not a valid image file, or could not be stored")
return True, None
@@ -671,6 +689,7 @@ def do_download_file(book, book_format, client, data, headers):
# ToDo Check headers parameter
for element in headers:
response.headers[element[0]] = element[1]
log.info('Downloading file: {}'.format(os.path.join(filename, data.name + "." + book_format)))
return response
##################################
@@ -684,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')
@@ -710,7 +726,6 @@ def json_serial(obj):
'seconds': obj.seconds,
'microseconds': obj.microseconds,
}
# return obj.isoformat()
raise TypeError("Type %s not serializable" % type(obj))
@@ -734,8 +749,8 @@ def format_runtime(runtime):
# helper function to apply localize status information in tasklist entries
def render_task_status(tasklist):
renderedtasklist = list()
for num, user, added, task in tasklist:
if user == current_user.nickname or current_user.role_admin():
for __, user, __, task in tasklist:
if user == current_user.name or current_user.role_admin():
ret = {}
if task.start_time:
ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())
@@ -756,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
@@ -773,8 +788,8 @@ def tags_filters():
# checks if domain is in database (including wildcards)
# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
# in all calls the email address is checked for validity
def check_valid_domain(domain_text):
# domain_text = domain_text.split('@', 1)[-1].lower()
sql = "SELECT * FROM registration WHERE (:domain LIKE domain and allow = 1);"
result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all()
if not len(result):
@@ -808,6 +823,7 @@ def get_download_link(book_id, book_format, client):
if book:
data1 = calibre_db.get_book_format(book.id, book_format.upper())
else:
log.error("Book id {} not found for downloading".format(book_id))
abort(404)
if data1:
# collect downloaded books only for registered user and not for anonymous user
@@ -815,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" % (
+11 -10
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
@@ -57,27 +55,30 @@ def get_language_name(locale, lang_code):
def get_language_codes(locale, language_names, remainder=None):
language_names = set(x.strip().lower() for x in language_names if x)
languages = list()
lang = list()
for k, v in get_language_names(locale).items():
v = v.lower()
if v in language_names:
languages.append(k)
lang.append(k)
language_names.remove(v)
if remainder is not None:
if remainder is not None and language_names:
remainder.extend(language_names)
return languages
return lang
def get_valid_language_codes(locale, language_names, remainder=None):
languages = list()
lang = list()
if "" in language_names:
language_names.remove("")
for k, v in get_language_names(locale).items():
for k, __ in get_language_names(locale).items():
if k in language_names:
languages.append(k)
lang.append(k)
language_names.remove(k)
if remainder is not None and len(language_names):
remainder.extend(language_names)
return languages
return lang
def get_lang3(lang):
try:
+1165 -419
View File
File diff suppressed because it is too large Load Diff
+14 -10
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
@@ -82,7 +81,7 @@ def formatdate_filter(val):
except AttributeError as e:
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
current_user.locale,
current_user.nickname
current_user.name
)
return val
@@ -113,21 +112,26 @@ 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')
def formatseriesindex_filter(series_index):
if series_index:
if int(series_index) - series_index == 0:
return int(series_index)
else:
try:
if int(series_index) - series_index == 0:
return int(series_index)
else:
return series_index
except ValueError:
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()
+179 -80
View File
@@ -19,7 +19,6 @@
import base64
import datetime
import sys
import os
import uuid
from time import gmtime, strftime
@@ -43,11 +42,13 @@ from flask_login import current_user
from werkzeug.datastructures import Headers
from sqlalchemy import func
from sqlalchemy.sql.expression import and_, or_
from sqlalchemy.orm import load_only
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
@@ -82,6 +83,7 @@ CONNECTION_SPECIFIC_HEADERS = [
"transfer-encoding",
]
def get_kobo_activated():
return config.config_kobo_sync
@@ -136,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')
@@ -152,30 +155,65 @@ 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))
.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))
.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)
kobo_reading_state = get_or_create_reading_state(book.Books.id)
entitlement = {
"BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
@@ -187,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})
@@ -195,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({
@@ -234,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):
@@ -259,12 +327,13 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
except Exception as e:
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
except Exception as ex:
log.error("Failed to receive or parse response from Kobo's sync endpoint: {}".format(ex))
if set_cont:
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
@@ -302,7 +371,8 @@ def get_download_url_for_book(book, book_format):
book_format=book_format.lower()
)
return url_for(
"web.download_link",
"kobo.download_book",
auth_token=kobo_auth.get_auth_token(),
book_id=book.id,
book_format=book_format.lower(),
_external=True,
@@ -388,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,
@@ -416,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() ?
@@ -430,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.
@@ -465,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):
@@ -520,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):
@@ -549,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):
@@ -595,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": {
@@ -610,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
@@ -657,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):
@@ -832,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):
@@ -854,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)
@@ -865,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"])
@@ -875,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"])
@@ -889,17 +999,6 @@ def HandleProductsRequest(dummy=None):
return redirect_or_proxy_request()
'''@kobo.errorhandler(404)
def handle_404(err):
# This handler acts as a catch-all for endpoints that we don't have an interest in
# implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc)
if err:
print('404')
return jsonify(error=str(err)), 404
log.debug("Unknown Request received: %s, method: %s, data: %s", request.base_url, request.method, request.data)
return redirect_or_proxy_request()'''
def make_calibre_web_auth_response():
# As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for
# authentation (nor for authorization). We return a dummy response just to keep the device happy.
@@ -919,6 +1018,7 @@ def make_calibre_web_auth_response():
)
@csrf.exempt
@kobo.route("/v1/auth/device", methods=["POST"])
@requires_kobo_auth
def HandleAuthRequest():
@@ -926,7 +1026,7 @@ def HandleAuthRequest():
if config.config_kobo_proxy:
try:
return redirect_or_proxy_request()
except:
except Exception:
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
return make_calibre_web_auth_response()
@@ -943,7 +1043,7 @@ def HandleInitRequest():
store_response_json = store_response.json()
if "Resources" in store_response_json:
kobo_resources = store_response_json["Resources"]
except:
except Exception:
log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.")
if not kobo_resources:
kobo_resources = NATIVE_KOBO_RESOURCES()
@@ -1004,7 +1104,6 @@ def HandleInitRequest():
@requires_kobo_auth
@download_required
def download_book(book_id, book_format):
return get_download_link(book_id, book_format, "kobo")
+11 -2
View File
@@ -64,10 +64,10 @@ from datetime import datetime
from os import urandom
from flask import g, Blueprint, url_for, abort, request
from flask_login import login_user, login_required
from flask_login import login_user, current_user, login_required
from flask_babel import gettext as _
from . import logger, ub, lm
from . import logger, config, calibre_db, db, helper, ub, lm
from .render_template import render_title_template
try:
@@ -81,6 +81,7 @@ log = logger.create()
def register_url_value_preprocessor(kobo):
@kobo.url_value_preprocessor
# pylint: disable=unused-variable
def pop_auth_token(__, values):
g.auth_token = values.pop("auth_token")
@@ -148,6 +149,14 @@ def generate_auth_token(user_id):
ub.session.add(auth_token)
ub.session_commit()
books = calibre_db.session.query(db.Books).join(db.Data).all()
for book in books:
formats = [data.format for data in book.data]
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
return render_title_template(
"generate_kobo_auth_url.html",
title=_(u"Kobo Setup"),
+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()
+5 -6
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
@@ -62,11 +61,11 @@ class _Logger(logging.Logger):
def debug_no_auth(self, message, *args, **kwargs):
message = message.strip("\r\n")
if message.startswith("send: AUTH"):
self.debug(message[:16], stacklevel=2, *args, **kwargs)
self.debug(message[:16], *args, **kwargs)
else:
self.debug(message, stacklevel=2, *args, **kwargs)
self.debug(message, *args, **kwargs)
def get(name=None):
@@ -153,11 +152,11 @@ def setup(log_file, log_level=None):
file_handler.baseFilename = log_file
else:
try:
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
file_handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=2, encoding='utf-8')
except IOError:
if log_file == DEFAULT_LOG_FILE:
raise
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2, encoding='utf-8')
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=100000, backupCount=2, encoding='utf-8')
log_file = ""
file_handler.setFormatter(FORMATTER)
+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
+117 -122
View File
@@ -16,10 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
from __future__ import division, print_function, unicode_literals
from flask import session
try:
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound
@@ -34,134 +32,131 @@ except ImportError:
except ImportError:
pass
try:
class OAuthBackend(SQLAlchemyBackend):
"""
Stores and retrieves OAuth tokens using a relational database through
the `SQLAlchemy`_ ORM.
.. _SQLAlchemy: https://www.sqlalchemy.org/
"""
def __init__(self, model, session, provider_id,
user=None, user_id=None, user_required=None, anon_user=None,
cache=None):
self.provider_id = provider_id
super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache)
class OAuthBackend(SQLAlchemyBackend):
"""
Stores and retrieves OAuth tokens using a relational database through
the `SQLAlchemy`_ ORM.
def get(self, blueprint, user=None, user_id=None):
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '':
return session[self.provider_id + '_oauth_token']
# check cache
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
token = self.cache.get(cache_key)
if token:
return token
# if not cached, make database queries
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
use_provider_user_id = False
if self.provider_id + '_oauth_user_id' in session and session[self.provider_id + '_oauth_user_id'] != '':
query = query.filter_by(provider_user_id=session[self.provider_id + '_oauth_user_id'])
use_provider_user_id = True
if self.user_required and not u and not uid and not use_provider_user_id:
# raise ValueError("Cannot get OAuth token without an associated user")
return None
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
try:
token = query.one().token
except NoResultFound:
token = None
# cache the result
self.cache.set(cache_key, token)
.. _SQLAlchemy: https://www.sqlalchemy.org/
"""
def __init__(self, model, session, provider_id,
user=None, user_id=None, user_required=None, anon_user=None,
cache=None):
self.provider_id = provider_id
super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache)
def get(self, blueprint, user=None, user_id=None):
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '':
return session[self.provider_id + '_oauth_token']
# check cache
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
token = self.cache.get(cache_key)
if token:
return token
def set(self, blueprint, token, user=None, user_id=None):
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
# if not cached, make database queries
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid:
raise ValueError("Cannot set OAuth token without an associated user")
use_provider_user_id = False
if self.provider_id + '_oauth_user_id' in session and session[self.provider_id + '_oauth_user_id'] != '':
query = query.filter_by(provider_user_id=session[self.provider_id + '_oauth_user_id'])
use_provider_user_id = True
# if there was an existing model, delete it
existing_query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
# check for user ID
has_user_id = hasattr(self.model, "user_id")
if has_user_id and uid:
existing_query = existing_query.filter_by(user_id=uid)
# check for user (relationship property)
has_user = hasattr(self.model, "user")
if has_user and u:
existing_query = existing_query.filter_by(user=u)
# queue up delete query -- won't be run until commit()
existing_query.delete()
# create a new model for this token
kwargs = {
"provider": self.provider_id,
"token": token,
}
if has_user_id and uid:
kwargs["user_id"] = uid
if has_user and u:
kwargs["user"] = u
self.session.add(self.model(**kwargs))
# commit to delete and add simultaneously
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id
))
if self.user_required and not u and not uid and not use_provider_user_id:
# raise ValueError("Cannot get OAuth token without an associated user")
return None
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
try:
token = query.one().token
except NoResultFound:
token = None
def delete(self, blueprint, user=None, user_id=None):
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
# cache the result
self.cache.set(cache_key, token)
if self.user_required and not u and not uid:
raise ValueError("Cannot delete OAuth token without an associated user")
return token
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
query.delete()
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id,
))
def set(self, blueprint, token, user=None, user_id=None):
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
except Exception:
pass
if self.user_required and not u and not uid:
raise ValueError("Cannot set OAuth token without an associated user")
# if there was an existing model, delete it
existing_query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
# check for user ID
has_user_id = hasattr(self.model, "user_id")
if has_user_id and uid:
existing_query = existing_query.filter_by(user_id=uid)
# check for user (relationship property)
has_user = hasattr(self.model, "user")
if has_user and u:
existing_query = existing_query.filter_by(user=u)
# queue up delete query -- won't be run until commit()
existing_query.delete()
# create a new model for this token
kwargs = {
"provider": self.provider_id,
"token": token,
}
if has_user_id and uid:
kwargs["user_id"] = uid
if has_user and u:
kwargs["user"] = u
self.session.add(self.model(**kwargs))
# commit to delete and add simultaneously
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id
))
def delete(self, blueprint, user=None, user_id=None):
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid:
raise ValueError("Cannot delete OAuth token without an associated user")
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
query.delete()
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id,
))
+170 -149
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
@@ -30,15 +29,20 @@ from flask_babel import gettext as _
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.contrib.google import make_google_blueprint, google
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
from flask_login import login_user, current_user, login_required
from sqlalchemy.orm.exc import NoResultFound
from . import constants, logger, config, app, ub
from .oauth import OAuthBackend, backend_resultcode
try:
from .oauth import OAuthBackend, backend_resultcode
except NameError:
pass
oauth_check = {}
oauthblueprints = []
oauth = Blueprint('oauth', __name__)
log = logger.create()
@@ -84,7 +88,7 @@ def register_user_with_oauth(user=None):
except NoResultFound:
# no found, return error
return
ub.session_commit("User {} with OAuth for provider {} registered".format(user.nickname, oauth_key))
ub.session_commit("User {} with OAuth for provider {} registered".format(user.name, oauth_key))
def logout_oauth_user():
@@ -93,8 +97,115 @@ def logout_oauth_user():
session.pop(str(oauth_key) + '_oauth_user_id')
if ub.oauth_support:
oauthblueprints = []
def oauth_update_token(provider_id, token, provider_user_id):
session[provider_id + "_oauth_user_id"] = provider_user_id
session[provider_id + "_oauth_token"] = token
# Find this OAuth token in the database, or create it
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
)
try:
oauth_entry = query.one()
# update token
oauth_entry.token = token
except NoResultFound:
oauth_entry = ub.OAuth(
provider=provider_id,
provider_user_id=provider_user_id,
token=token,
)
ub.session.add(oauth_entry)
ub.session_commit()
# Disable Flask-Dance's default behavior for saving the OAuth token
# Value differrs depending on flask-dance version
return backend_resultcode
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
)
try:
oauth_entry = query.first()
# already bind with user, just login
if oauth_entry.user:
login_user(oauth_entry.user)
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name),
category="success")
return redirect(url_for('web.index'))
else:
# bind to current user
if current_user and current_user.is_authenticated:
oauth_entry.user = current_user
try:
ub.session.add(oauth_entry)
ub.session.commit()
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
log.info("Link to {} Succeeded".format(provider_name))
return redirect(url_for('web.profile'))
except Exception as ex:
log.debug_or_exception(ex)
ub.session.rollback()
else:
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
log.info('Login failed, No User Linked With OAuth Account')
return redirect(url_for('web.login'))
# return redirect(url_for('web.login'))
# if config.config_public_reg:
# return redirect(url_for('web.register'))
# else:
# flash(_(u"Public registration is not enabled"), category="error")
# return redirect(url_for(redirect_url))
except (NoResultFound, AttributeError):
return redirect(url_for(redirect_url))
def get_oauth_status():
status = []
query = ub.session.query(ub.OAuth).filter_by(
user_id=current_user.id,
)
try:
oauths = query.all()
for oauth_entry in oauths:
status.append(int(oauth_entry.provider))
return status
except NoResultFound:
return None
def unlink_oauth(provider):
if request.host_url + 'me' != request.referrer:
pass
query = ub.session.query(ub.OAuth).filter_by(
provider=provider,
user_id=current_user.id,
)
try:
oauth_entry = query.one()
if current_user and current_user.is_authenticated:
oauth_entry.user = current_user
try:
ub.session.delete(oauth_entry)
ub.session.commit()
logout_oauth_user()
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
except Exception as ex:
log.debug_or_exception(ex)
ub.session.rollback()
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound:
log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile'))
def generate_oauth_blueprints():
if not ub.session.query(ub.OAuthProvider).count():
for provider in ("github", "google"):
oauthProvider = ub.OAuthProvider()
@@ -138,17 +249,23 @@ if ub.oauth_support:
app.register_blueprint(blueprint, url_prefix="/login")
if element['active']:
register_oauth_blueprint(element['id'], element['provider_name'])
return oauthblueprints
if ub.oauth_support:
oauthblueprints = generate_oauth_blueprints()
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
def github_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with GitHub."), category="error")
log.error("Failed to log in with GitHub")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error")
log.error("Failed to fetch user info from GitHub")
return False
github_info = resp.json()
@@ -160,11 +277,13 @@ if ub.oauth_support:
def google_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with Google."), category="error")
log.error("Failed to log in with Google")
return False
resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error")
log.error("Failed to fetch user info from Google")
return False
google_info = resp.json()
@@ -172,113 +291,6 @@ if ub.oauth_support:
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
def oauth_update_token(provider_id, token, provider_user_id):
session[provider_id + "_oauth_user_id"] = provider_user_id
session[provider_id + "_oauth_token"] = token
# Find this OAuth token in the database, or create it
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
)
try:
oauth_entry = query.one()
# update token
oauth_entry.token = token
except NoResultFound:
oauth_entry = ub.OAuth(
provider=provider_id,
provider_user_id=provider_user_id,
token=token,
)
ub.session.add(oauth_entry)
ub.session_commit()
# Disable Flask-Dance's default behavior for saving the OAuth token
# Value differrs depending on flask-dance version
return backend_resultcode
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
)
try:
oauth_entry = query.first()
# already bind with user, just login
if oauth_entry.user:
login_user(oauth_entry.user)
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname),
category="success")
return redirect(url_for('web.index'))
else:
# bind to current user
if current_user and current_user.is_authenticated:
oauth_entry.user = current_user
try:
ub.session.add(oauth_entry)
ub.session.commit()
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
return redirect(url_for('web.profile'))
except Exception as e:
log.debug_or_exception(e)
ub.session.rollback()
else:
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
log.info('Login failed, No User Linked With OAuth Account')
return redirect(url_for('web.login'))
# return redirect(url_for('web.login'))
# if config.config_public_reg:
# return redirect(url_for('web.register'))
# else:
# flash(_(u"Public registration is not enabled"), category="error")
# return redirect(url_for(redirect_url))
except (NoResultFound, AttributeError):
return redirect(url_for(redirect_url))
def get_oauth_status():
status = []
query = ub.session.query(ub.OAuth).filter_by(
user_id=current_user.id,
)
try:
oauths = query.all()
for oauth_entry in oauths:
status.append(int(oauth_entry.provider))
return status
except NoResultFound:
return None
def unlink_oauth(provider):
if request.host_url + 'me' != request.referrer:
pass
query = ub.session.query(ub.OAuth).filter_by(
provider=provider,
user_id=current_user.id,
)
try:
oauth_entry = query.one()
if current_user and current_user.is_authenticated:
oauth_entry.user = current_user
try:
ub.session.delete(oauth_entry)
ub.session.commit()
logout_oauth_user()
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
except Exception as e:
log.debug_or_exception(e)
ub.session.rollback()
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound:
log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile'))
# notify on OAuth provider error
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
def github_error(blueprint, error, error_description=None, error_uri=None):
@@ -293,39 +305,6 @@ if ub.oauth_support:
) # ToDo: Translate
flash(msg, category="error")
@oauth.route('/link/github')
@oauth_required
def github_login():
if not github.authorized:
return redirect(url_for('github.login'))
account_info = github.get('/user')
if account_info.ok:
account_info_json = account_info.json()
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
flash(_(u"GitHub Oauth error, please retry later."), category="error")
return redirect(url_for('web.login'))
@oauth.route('/unlink/github', methods=["GET"])
@login_required
def github_login_unlink():
return unlink_oauth(oauthblueprints[0]['id'])
@oauth.route('/link/google')
@oauth_required
def google_login():
if not google.authorized:
return redirect(url_for("google.login"))
resp = google.get("/oauth2/v2/userinfo")
if resp.ok:
account_info_json = resp.json()
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
flash(_(u"Google Oauth error, please retry later."), category="error")
return redirect(url_for('web.login'))
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
def google_error(blueprint, error, error_description=None, error_uri=None):
msg = (
@@ -340,7 +319,49 @@ if ub.oauth_support:
flash(msg, category="error")
@oauth.route('/unlink/google', methods=["GET"])
@login_required
def google_login_unlink():
return unlink_oauth(oauthblueprints[1]['id'])
@oauth.route('/link/github')
@oauth_required
def github_login():
if not github.authorized:
return redirect(url_for('github.login'))
try:
account_info = github.get('/user')
if account_info.ok:
account_info_json = account_info.json()
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
flash(_(u"GitHub Oauth error, please retry later."), category="error")
log.error("GitHub Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e:
flash(_(u"GitHub Oauth error: {}").format(e), category="error")
log.error(e)
return redirect(url_for('web.login'))
@oauth.route('/unlink/github', methods=["GET"])
@login_required
def github_login_unlink():
return unlink_oauth(oauthblueprints[0]['id'])
@oauth.route('/link/google')
@oauth_required
def google_login():
if not google.authorized:
return redirect(url_for("google.login"))
try:
resp = google.get("/oauth2/v2/userinfo")
if resp.ok:
account_info_json = resp.json()
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
flash(_(u"Google Oauth error, please retry later."), category="error")
log.error("Google Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e:
flash(_(u"Google Oauth error: {}").format(e), category="error")
log.error(e)
return redirect(url_for('web.login'))
@oauth.route('/unlink/google', methods=["GET"])
@login_required
def google_login_unlink():
return unlink_oauth(oauthblueprints[1]['id'])
+152 -35
View File
@@ -20,14 +20,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys
import datetime
from functools import wraps
from flask import Blueprint, request, render_template, Response, g, make_response, abort
from flask_login import current_user
from sqlalchemy.sql.expression import func, text, or_, and_
from sqlalchemy.sql.expression import func, text, or_, and_, true
from werkzeug.security import check_password_hash
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
@@ -94,7 +93,45 @@ def feed_cc_search(query):
@opds.route("/opds/search", methods=["GET"])
@requires_basic_auth_if_no_ano
def feed_normal_search():
return feed_search(request.args.get("query").strip())
return feed_search(request.args.get("query", "").strip())
@opds.route("/opds/books")
@requires_basic_auth_if_no_ano
def feed_booksindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Books.sort, 1, 1)).label('id'))\
.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(db.Books.sort, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_books',
pagination=pagination)
@opds.route("/opds/books/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_books(book_id):
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Books.sort).startswith(book_id)
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
letter,
[db.Books.sort])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/new")
@@ -150,14 +187,41 @@ def feed_hot():
@opds.route("/opds/author")
@requires_basic_auth_if_no_ano
def feed_authorindex():
off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
.filter(calibre_db.common_filters())\
.group_by(text('books_authors_link.author'))\
.order_by(db.Authors.sort).limit(config.config_books_per_page)\
.offset(off)
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('id'))\
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters())\
.group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Authors).all()))
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_author',
pagination=pagination)
@opds.route("/opds/author/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_author(book_id):
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id)
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
.filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_authors_link.author'))\
.order_by(db.Authors.sort)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
entries.count())
entries = entries.limit(config.config_books_per_page).offset(off).all()
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
@@ -201,17 +265,41 @@ def feed_publisher(book_id):
@opds.route("/opds/category")
@requires_basic_auth_if_no_ano
def feed_categoryindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('id'))\
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters())\
.group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_category',
pagination=pagination)
@opds.route("/opds/category/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_category(book_id):
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id)
entries = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
.join(db.Books)\
.filter(calibre_db.common_filters())\
.filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_tags_link.tag'))\
.order_by(db.Tags.name)\
.offset(off)\
.limit(config.config_books_per_page)
.order_by(db.Tags.name)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Tags).all()))
entries.count())
entries = entries.offset(off).limit(config.config_books_per_page).all()
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination)
@@ -229,16 +317,40 @@ def feed_category(book_id):
@opds.route("/opds/series")
@requires_basic_auth_if_no_ano
def feed_seriesindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('id'))\
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters())\
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_series',
pagination=pagination)
@opds.route("/opds/series/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_series(book_id):
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id)
entries = calibre_db.session.query(db.Series)\
.join(db.books_series_link)\
.join(db.Books)\
.filter(calibre_db.common_filters())\
.filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_series_link.series'))\
.order_by(db.Series.sort)\
.offset(off).all()
.order_by(db.Series.sort)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Series).all()))
entries.count())
entries = entries.offset(off).limit(config.config_books_per_page).all()
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination)
@@ -269,7 +381,7 @@ def feed_ratingindex():
len(entries))
element = list()
for entry in entries:
element.append(FeedObject(entry[0].id, "{} Stars".format(entry.name)))
element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name)))
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
@@ -320,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)
@@ -423,14 +536,18 @@ 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')
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) ==
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()
return bool(user and check_password_hash(str(user.password), password))
if bool(user and check_password_hash(str(user.password), password)):
return True
else:
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_Address)
return False
def authenticate():
-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:
+3 -3
View File
@@ -62,7 +62,7 @@ def remote_login():
ub.session_commit()
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
return render_title_template('remote_login.html', title=_(u"Login"), token=auth_token.auth_token,
verify_url=verify_url, page="remotelogin")
@@ -126,11 +126,11 @@ def token_verified():
login_user(user)
ub.session.delete(auth_token)
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.nickname))
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
data['status'] = 'success'
log.debug(u"Remote Login for userid %s succeded", user.id)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8"
+12 -6
View File
@@ -36,16 +36,22 @@ def get_sidebar_config(kwargs=None):
else:
content = 'conf' in kwargs
sidebar = list()
sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new",
sidebar.append({"glyph": "glyphicon-book", "text": _('Books'), "link": 'web.index', "id": "new",
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
"show_text": _('Show recent books'), "config_show":False})
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
"show_text": _('Show Hot Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
if current_user.role_admin():
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
else:
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
sidebar.append(
{"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated",
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
@@ -59,7 +65,7 @@ def get_sidebar_config(kwargs=None):
"show_text": _('Show unread'), "config_show": False})
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
"show_text": _('Show random books'), "config_show": True})
"show_text": _('Show Random Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
"show_text": _('Show category selection'), "config_show": True})
-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')
+64 -8
View File
@@ -16,12 +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
import errno
import signal
import socket
import subprocess # nosec
try:
from gevent.pywsgi import WSGIServer
@@ -136,6 +136,64 @@ class WebServer(object):
return sock, _readable_listen_address(*address)
@staticmethod
def _get_args_for_reloading():
"""Determine how the script was executed, and return the args needed
to execute it again in a new process.
Code from https://github.com/pyload/pyload. Author GammaC0de, voulter
"""
rv = [sys.executable]
py_script = sys.argv[0]
args = sys.argv[1:]
# Need to look at main module to determine how it was executed.
__main__ = sys.modules["__main__"]
# The value of __package__ indicates how Python was called. It may
# not exist if a setuptools script is installed as an egg. It may be
# set incorrectly for entry points created with pip on Windows.
if getattr(__main__, "__package__", None) is None or (
os.name == "nt"
and __main__.__package__ == ""
and not os.path.exists(py_script)
and os.path.exists("{}.exe".format(py_script))
):
# Executed a file, like "python app.py".
py_script = os.path.abspath(py_script)
if os.name == "nt":
# Windows entry points have ".exe" extension and should be
# called directly.
if not os.path.exists(py_script) and os.path.exists("{}.exe".format(py_script)):
py_script += ".exe"
if (
os.path.splitext(sys.executable)[1] == ".exe"
and os.path.splitext(py_script)[1] == ".exe"
):
rv.pop(0)
rv.append(py_script)
else:
# Executed a module, like "python -m module".
if sys.argv[0] == "-m":
args = sys.argv
else:
if os.path.isfile(py_script):
# Rewritten by Python from "-m script" to "/path/to/script.py".
py_module = __main__.__package__
name = os.path.splitext(os.path.basename(py_script))[0]
if name != "__main__":
py_module += ".{}".format(name)
else:
# Incorrectly rewritten by pydevd debugger from "-m script" to "script".
py_module = py_script
rv.extend(("-m", py_module.lstrip(".")))
rv.extend(args)
return rv
def _start_gevent(self):
ssl_args = self.ssl_args or {}
@@ -192,18 +250,16 @@ class WebServer(object):
finally:
self.wsgiserver = None
# prevent irritating log of pending tasks message from asyncio
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
if not self.restart:
log.info("Performing shutdown of Calibre-Web")
# prevent irritiating log of pending tasks message from asyncio
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
return True
log.info("Performing restart of Calibre-Web")
arguments = list(sys.argv)
arguments.insert(0, sys.executable)
if os.name == 'nt':
arguments = ["\"%s\"" % a for a in arguments]
os.execv(sys.executable, arguments)
args = self._get_args_for_reloading()
subprocess.call(args, close_fds=True) # nosec
return True
def _killServer(self, __, ___):
+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
+24 -17
View File
@@ -22,6 +22,7 @@ from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__
from datetime import datetime
try:
# pylint: disable=unused-import
from urllib import unquote
except ImportError:
from urllib.parse import unquote
@@ -34,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.
@@ -64,7 +62,7 @@ class SyncToken:
books_last_modified: Datetime representing the last modified book that the device knows about.
"""
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
SYNC_TOKEN_HEADER = "x-kobo-synctoken" # nosec
VERSION = "1-1-0"
LAST_MODIFIED_ADDED_VERSION = "1-1-0"
MIN_VERSION = "1-0-0"
@@ -84,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}
},
}
@@ -96,21 +94,21 @@ 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
self.books_last_modified = books_last_modified
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):
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
if sync_token_header == "":
if sync_token_header == "": # nosec
return SyncToken()
# On the first sync from a Kobo device, we may receive the SyncToken
@@ -140,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,
@@ -154,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):
@@ -178,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)
+6 -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
@@ -45,3 +43,9 @@ except ImportError as err:
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
kobo = None
SyncToken = None
try:
from . import gmail
except ImportError as err:
log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err)
gmail = None
+100
View File
@@ -0,0 +1,100 @@
# -*- 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
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from datetime import datetime
import base64
from flask_babel import gettext as _
from ..constants import BASE_DIR
from .. import logger
log = logger.create()
SCOPES = ['openid', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/userinfo.email']
def setup_gmail(token):
# If there are no (valid) credentials available, let the user log in.
creds = None
if "token" in token:
creds = Credentials(
token=token['token'],
refresh_token=token['refresh_token'],
token_uri=token['token_uri'],
client_id=token['client_id'],
client_secret=token['client_secret'],
scopes=token['scopes'],
)
creds.expiry = datetime.fromisoformat(token['expiry'])
if not creds or not creds.valid:
# don't forget to dump one more time after the refresh
# also, some file-locking routines wouldn't be needless
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
cred_file = os.path.join(BASE_DIR, 'gmail.json')
if not os.path.exists(cred_file):
raise Exception(_("Found no valid gmail.json file with OAuth information"))
flow = InstalledAppFlow.from_client_secrets_file(
os.path.join(BASE_DIR, 'gmail.json'), SCOPES)
creds = flow.run_local_server(port=0)
user_info = get_user_info(creds)
return {
'token': creds.token,
'refresh_token': creds.refresh_token,
'token_uri': creds.token_uri,
'client_id': creds.client_id,
'client_secret': creds.client_secret,
'scopes': creds.scopes,
'expiry': creds.expiry.isoformat(),
'email': user_info
}
return {}
def get_user_info(credentials):
user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials)
user_info = user_info_service.userinfo().get().execute()
return user_info.get('email', "")
def send_messsage(token, msg):
log.debug("Start sending e-mail via Gmail")
creds = Credentials(
token=token['token'],
refresh_token=token['refresh_token'],
token_uri=token['token_uri'],
client_id=token['client_id'],
client_secret=token['client_secret'],
scopes=token['scopes'],
)
creds.expiry = datetime.fromisoformat(token['expiry'])
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
service = build('gmail', 'v1', credentials=creds)
message_as_bytes = msg.as_bytes() # the message should converted from string to bytes.
message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) # encode in base64 (printable letters coding)
raw = message_as_base64.decode() # convert to something JSON serializable
body = {'raw': raw}
(service.users().messages().send(userId='me', body=body).execute())
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
+29 -9
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
@@ -45,7 +61,7 @@ class ImprovedQueue(queue.Queue):
with self.mutex:
return list(self.queue)
#Class for all worker tasks in the background
# Class for all worker tasks in the background
class WorkerThread(threading.Thread):
_instance = None
@@ -69,6 +85,7 @@ class WorkerThread(threading.Thread):
def add(cls, user, task):
ins = cls.getInstance()
ins.num += 1
log.debug("Add Task for user: {}: {}".format(user, task))
ins.queue.put(QueuedTask(
num=ins.num,
user=user,
@@ -159,9 +176,9 @@ class CalibreTask:
# catch any unhandled exceptions in a task and automatically fail it
try:
self.run(*args)
except Exception as e:
self._handleError(str(e))
log.debug_or_exception(e)
except Exception as ex:
self._handleError(str(ex))
log.debug_or_exception(ex)
self.end_time = datetime.now()
@@ -204,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
+75 -52
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()
@@ -99,12 +96,14 @@ def add_to_shelf(shelf_id, book_id):
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
log.error("Settings DB is not Writeable")
flash(_(u"Settings DB is not Writeable"), category="error")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
return redirect(url_for('web.index'))
if not xhr:
log.debug("Book has been added to shelf: {}".format(shelf.name))
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
@@ -123,6 +122,7 @@ def search_to_shelf(shelf_id):
return redirect(url_for('web.index'))
if not check_shelf_edit_permissions(shelf):
log.warning("You are not allowed to add a book to the the shelf: {}".format(shelf.name))
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
@@ -140,7 +140,7 @@ def search_to_shelf(shelf_id):
books_for_shelf = ub.searched_ids[current_user.id]
if not books_for_shelf:
log.error("Books are already part of %s", shelf.name)
log.error("Books are already part of {}".format(shelf.name))
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
@@ -156,8 +156,10 @@ def search_to_shelf(shelf_id):
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
else:
log.error("Could not add books to shelf: {}".format(shelf.name))
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
return redirect(url_for('web.index'))
@@ -168,7 +170,7 @@ def remove_from_shelf(shelf_id, book_id):
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if shelf is None:
log.error("Invalid shelf specified: %s", shelf_id)
log.error("Invalid shelf specified: {}".format(shelf_id))
if not xhr:
return redirect(url_for('web.index'))
return "Invalid shelf specified", 400
@@ -197,7 +199,8 @@ def remove_from_shelf(shelf_id, book_id):
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
@@ -211,6 +214,7 @@ def remove_from_shelf(shelf_id, book_id):
return "", 204
else:
if not xhr:
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
category="error")
return redirect(url_for('web.index'))
@@ -221,74 +225,86 @@ 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 e:
except (OperationalError, InvalidRequestError) as ex:
ub.session.rollback()
log.debug_or_exception(e)
flash(_(u"Settings DB is not Writeable"), category="error")
except Exception as e:
log.debug_or_exception(ex)
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
except Exception as ex:
ub.session.rollback()
log.debug_or_exception(e)
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:
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:
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
@@ -311,7 +327,8 @@ def delete_shelf(shelf_id):
delete_shelf_helper(cur_shelf)
except InvalidRequestError:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
return redirect(url_for('web.index'))
@@ -345,13 +362,14 @@ def order_shelf(shelf_id):
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
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,
@@ -360,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) \
@@ -390,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:
@@ -400,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))
@@ -415,7 +437,8 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
return render_title_template(page,
entries=result,
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