Compare commits

..
336 Commits
Author SHA1 Message Date
Ozzieisaacs 0735fb1e92 Fix #1349 (Fix error on move cover with foreign file systems, e.g. samba shares)
Preparation for release
2020-05-05 20:28:10 +02:00
Ozzieisaacs 850a85915b Fix #1354 (Error on uploading single book, because of missing rarfile) 2020-05-05 18:48:40 +02:00
Ozzieisaacs fcbeeca305 Merge remote-tracking branch 'it/patch-11' 2020-05-04 20:21:05 +02:00
Ozzieisaacs fb16429867 Randomize flask secret_key 2020-05-04 19:02:03 +02:00
ElQuimm db38d7ee78 Updated version of italian.po
:-)
2020-05-03 21:30:06 +02:00
OzzieIsaacs 36a984ce3c Revert proxyfix 2020-05-02 18:17:52 +02:00
Ozzieisaacs 1ce432b136 Merge remote-tracking branch 'kobo/fix_covers_merge' 2020-05-02 09:34:24 +02:00
Michael Shavit e0fbfa44a4 Fix issue with cover images introduced during the merge of #1277. 2020-05-02 01:55:14 -04:00
Ozzieisaacs 547bbecef1 Merge branch 'master' into Develop 2020-05-01 17:16:28 +02:00
Ozzieisaacs 700cb3b553 Merge remote-tracking branch 'NL/master'
Updated all strings
2020-05-01 17:15:59 +02:00
Ozzieisaacs 8646f8f23a Merge branch 'Develop'
# Conflicts:
#	cps/__init__.py
2020-05-01 14:51:54 +02:00
OzzieIsaacs 99cc69c67d Update Teststatus 2020-05-01 14:38:54 +02:00
OzzieIsaacs 2c5d76908a Added missing upload format 2020-05-01 13:34:16 +02:00
Ozzieisaacs 832b34fc54 Improved errorhandling for resending password
Improved errorhandling for editing user
2020-05-01 12:00:45 +02:00
Ozzieisaacs 000b85ff81 Fixes for deleting books(error handling and user feedback) 2020-05-01 10:26:35 +02:00
Ozzieisaacs bb317d54f2 Fix reject reset password without configured email server 2020-05-01 08:37:54 +02:00
Ozzieisaacs d6f41d8dc0 Fix error 404 on reset password (Fix #1342) 2020-05-01 08:33:50 +02:00
Marcel 6dff5ed679 NL language update 2020-04-30 21:30:50 +02:00
OzzieIsaacs fb8b6310d5 Fix from tests 2020-04-30 20:58:01 +02:00
Ozzieisaacs 02aaf17ac5 Fix #1339 (Proxyfix import with old werkzeug versions causes traceback) 2020-04-30 19:21:08 +02:00
Ozzieisaacs b160a8de0b Merge branch 'master' into Develop
# Conflicts:
#	cps/__init__.py
#	cps/comic.py
#	cps/editbooks.py
#	cps/helper.py
#	cps/kobo.py
#	cps/translations/nl/LC_MESSAGES/messages.mo
#	cps/translations/nl/LC_MESSAGES/messages.po
#	cps/ub.py
#	cps/uploader.py
#	cps/web.py
2020-04-30 18:08:28 +02:00
Ozzieisaacs e3246fd751 Merge remote-tracking branch 'key' 2020-04-30 17:29:41 +02:00
Ozzieisaacs 91b1775f50 Fix #1225 (Unrar binary added, cover files from cbr files can be extracted) 2020-04-29 19:02:35 +02:00
Ozzieisaacs fb18ab1ca5 Fix #866 (Recent book sidebar element can't be removed anymore) 2020-04-29 18:57:39 +02:00
Ozzieisaacs 01ff55c84e Removed non working filters for search 2020-04-29 17:57:53 +02:00
jvoisin 523aab2e9e Don't use an hardcoded session key
This fixes a trivial authentication bypass,
according to https://flask.palletsprojects.com/en/1.1.x/quickstart/#sessions
2020-04-29 13:59:34 +02:00
Ozzieisaacs 9a7d9da654 Merge remote-tracking branch 'js_caliblur/xss' 2020-04-29 12:08:51 +02:00
Ozzieisaacs e9446556a1 Merge remote-tracking branch 'ru/master' 2020-04-29 12:06:35 +02:00
jvoisin 806a5f209f Fix two minor xss 2020-04-29 11:33:33 +02:00
ZIzA c864b368b0 Russian language update 2020-04-29 01:30:14 +04:00
Ozzieisaacs 27eb09fb19 Add unrar Support 2020-04-28 20:23:39 +02:00
Ozzieisaacs bea7223a0a Renabled cbrimage extraction (via unrar and rarfile) 2020-04-28 16:50:08 +02:00
Ozzieisaacs 0297823bda Merge remote-tracking branch 'codecosmetic/move_var' 2020-04-28 15:33:25 +02:00
Ozzieisaacs d1bce5c2d9 Update nl translation from development branch 2020-04-27 21:15:19 +02:00
Ozzieisaacs 46c0ae3ccc Revert accidently deleted nl translation strings 2020-04-27 21:11:43 +02:00
Ozzieisaacs ce6b689147 Code cosmetics 2020-04-27 20:22:55 +02:00
Ozzieisaacs 2d92818613 Code cosmetics 2020-04-27 20:01:13 +02:00
jvoisin 487878819e Move a few variables around
This should prevent a couple of crashes
due to undeclared variables.
2020-04-27 12:03:54 +02:00
Ozzieisaacs 6682b1ced1 Fix download of book covers with enabled pillow from url 2020-04-26 21:00:29 +02:00
Ozzieisaacs bc89b0658a Fix deletion of books with multiple custom column links
Fix delete of author-folder in case last book of author was deleted (#1269)
2020-04-26 20:44:37 +02:00
Ozzieisaacs d9dde36c74 Merge remote-tracking branch 'improve_read/sort-book-name' search results are ordered according to sort field in book title 2020-04-26 19:00:36 +02:00
Ozzieisaacs 44284ea5fb Merge remote-tracking branch 'opds/master'
# Conflicts:
#	cps/translations/cs/LC_MESSAGES/messages.po
#	cps/translations/de/LC_MESSAGES/messages.po
#	cps/translations/es/LC_MESSAGES/messages.po
#	cps/translations/fi/LC_MESSAGES/messages.po
#	cps/translations/fr/LC_MESSAGES/messages.po
#	cps/translations/hu/LC_MESSAGES/messages.po
#	cps/translations/it/LC_MESSAGES/messages.po
#	cps/translations/ja/LC_MESSAGES/messages.po
#	cps/translations/km/LC_MESSAGES/messages.po
#	cps/translations/nl/LC_MESSAGES/messages.po
#	cps/translations/pl/LC_MESSAGES/messages.po
#	cps/translations/ru/LC_MESSAGES/messages.po
#	cps/translations/sv/LC_MESSAGES/messages.po
#	cps/translations/uk/LC_MESSAGES/messages.po
#	cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po
#	messages.pot
2020-04-26 18:55:23 +02:00
Ozzieisaacs 9f0c0b34af Merge remote-tracking branch 'order_category/master' 2020-04-26 18:50:28 +02:00
Ozzieisaacs 87ec44aed5 Merge remote-tracking branch 'improve_reaad/keep-focus-on-search-input' 2020-04-26 18:38:09 +02:00
Ozzieisaacs 898e6b4f80 Calibre-Web is rebooted everytime login type ic shanged (from and to standard authentication and between login methods) 2020-04-26 18:24:22 +02:00
Ozzieisaacs dea2600913 Merge remote-tracking branch 'oauth/patch-2' Fix oauth in combination with nginx reverse proxy 2020-04-26 18:23:16 +02:00
Ozzieisaacs dc46ad16ae Bugfix view shelfs on book detail page 2020-04-26 11:35:47 +02:00
Ozzieisaacs 456550a943 Bugfix view shelfs on detiled page
bugfix datetime in archive mode
2020-04-26 11:34:10 +02:00
Ozzieisaacs 7b789b3701 Fix opds with shelves
Fix errorhandler ldap without ldap
2020-04-26 08:40:53 +02:00
Ozzieisaacs 0480d493cf Fix opds login with ldap, if ldap server is down 2020-04-25 21:12:07 +02:00
Ozzieisaacs b4d7733e0a Merge branch 'master' into Develop
# Conflicts:
#	cps/templates/list.html
#	cps/translations/cs/LC_MESSAGES/messages.po
#	cps/translations/de/LC_MESSAGES/messages.po
#	cps/translations/es/LC_MESSAGES/messages.po
#	cps/translations/fi/LC_MESSAGES/messages.po
#	cps/translations/fr/LC_MESSAGES/messages.po
#	cps/translations/hu/LC_MESSAGES/messages.po
#	cps/translations/it/LC_MESSAGES/messages.po
#	cps/translations/ja/LC_MESSAGES/messages.po
#	cps/translations/km/LC_MESSAGES/messages.po
#	cps/translations/nl/LC_MESSAGES/messages.po
#	cps/translations/pl/LC_MESSAGES/messages.po
#	cps/translations/ru/LC_MESSAGES/messages.po
#	cps/translations/sv/LC_MESSAGES/messages.po
#	cps/translations/uk/LC_MESSAGES/messages.po
#	cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po
#	messages.pot
2020-04-25 20:06:25 +02:00
Ozzieisaacs 8b8fe7a0ae bugfix on create shelv via kobo sync protocol
bugfix for totally wrong json requests
prevent empty shelf names on kobo create shelf
Removed errorhandler 404
2020-04-25 20:02:55 +02:00
Ozzie Isaacs 85ffc90f66 Update markup 2020-04-25 08:56:51 +02:00
Ozzieisaacs fa38798066 Added contributing file 2020-04-25 08:55:01 +02:00
Ozzieisaacs d657330584 Fix for removing admin role from last admin user (in addition to prevent delete of last admin user) #1326 2020-04-25 07:13:55 +02:00
Ozzieisaacs 36cb79de62 Merge remote-tracking branch 'turkish/master' 2020-04-24 22:14:57 +02:00
Ozzieisaacs 063ee5e855 Merge remote-tracking branch 'remove_usage_local_var_undeclared'
Code cosmetics
2020-04-24 21:05:56 +02:00
Ozzieisaacs 7393b69757 Fix for #1326 (typo) 2020-04-24 19:25:11 +02:00
Ozzieisaacs 8bd1903d98 Fix for #1326 (opds feed user login not working -> download was not possible) 2020-04-24 18:37:29 +02:00
jvoisin d8bf540db2 Remove an undeclared and useless local variable 2020-04-24 18:04:27 +02:00
Ozzieisaacs e29f17ac46 Merge remote-tracking branch 'kobo/shelves' into Develop
# Conflicts:
#	cps/kobo.py
#	cps/shelf.py
2020-04-24 16:56:08 +02:00
iz 1770f3fd0d Update turkish translation headers 2020-04-23 22:47:24 +03:00
iz 4239f2ed71 Add turkish translation 2020-04-23 22:37:44 +03:00
iz 3939ec28ba Add turkish language names 2020-04-23 22:37:35 +03:00
Michael Shavit 742ec2b38d Fix issue with the delete_book changes introduced in
41a3623
2020-04-22 00:19:33 -04:00
Michael Shavit 9296d35517 Fix bug with shelf deletions for Kobo.
We were incorrectly setting the user_id field in the ShelfArchive table
2020-04-21 23:57:20 -04:00
Michael Shavit 06c15a792e Minor fixes to the Kobo shelves implementation (mostly typos) 2020-04-21 23:36:47 -04:00
Ozzieisaacs 95ca1e6a9d Fix for #1319 2020-04-20 18:56:39 +02:00
Ozzieisaacs 1df82110d1 Update Italian language translation 2020-04-19 19:13:10 +02:00
Ozzieisaacs 24c743d23d Code cosmetics 2020-04-19 19:08:58 +02:00
ElQuimm f1b1ebe29e Updated italian version of messages.po 2020-04-19 16:38:30 +02:00
Jeff 6384cdc74d Fix https github oauth
while using https domian and nginx as proxy, a `ProxyFix` is required.
2020-04-19 19:53:41 +08:00
celogeek d093fbb069 Keep previous search value and focus 2020-04-19 13:12:02 +02:00
celogeek f9b4505843 Sort search result by Books sort
It is far easier to search for a
series or anything and see
books in sort order
2020-04-19 13:03:46 +02:00
Ozzieisaacs 902685a197 Fix text in user config section (random books in Detail View) 2020-04-18 21:30:28 +02:00
Ozzieisaacs c04b146486 Fix #1309 (fix duplicate shelf names in book detail view) 2020-04-18 21:15:02 +02:00
Ozzieisaacs 7bb5afa585 Merge remote-tracking branch 'kobo/shelves' into Develop 2020-04-18 21:12:21 +02:00
Ozzieisaacs 06fde4fcd0 Fix #1311 (Enable LDAP Anonymous Login) 2020-04-18 11:48:34 +02:00
Ozzieisaacs 195845ab0c Fix 2020-04-17 18:17:35 +02:00
Ozzieisaacs 81a329f1e7 Fix for #1087 ( Caliblur! Theme button alignment) 2020-04-17 18:16:28 +02:00
OzzieIsaacs cd6272a1c9 Update teststatus 2020-04-16 22:00:39 +02:00
Ozzieisaacs 53be752787 Merge remote-tracking branch 'it/patch-10' 2020-04-16 20:17:02 +02:00
Ozzieisaacs 42ac06c114 Merge remote-tracking branch 'nl/Develop' into Develop 2020-04-16 20:14:56 +02:00
Ozzieisaacs 9e159ed5ab Merge branch 'master' into Develop
# Conflicts:
#	cps/config_sql.py
#	cps/ub.py
#	cps/web.py
2020-04-16 20:12:27 +02:00
Ozzieisaacs 2c42972230 Fix for #1307 (Oauth login not working)
- Changed blueprint reference in Backend from name to id, users can now again be found
- Depending on version of flask-dance the resultcode of authorized is set different to prevent redirect loop with newer versions(>1.3)
- Redirect to login page in case no user is linked to current oauth account
- Flash messages upon successfull login, successsfull linked account
2020-04-16 17:58:42 +02:00
aribes f926e58891 Changing ordering logic when showing books from a category 2020-04-16 11:37:04 +10:00
Ozzieisaacs a784c6bd52 Fixes for oauth login after programming basic tests 2020-04-15 22:03:21 +02:00
ElQuimm 05d78f5cb5 Update italian version of message.po
Have a nice day!
2020-04-15 10:59:08 +02:00
Ozzieisaacs ce4f1258b5 Fix #1293 (error 500 on search without query) 2020-04-14 19:27:18 +02:00
Ozzieisaacs 3fbaba6693 Fix #1307 2020-04-14 18:28:16 +02:00
Ozzieisaacs a8b36aed92 Temporary fix for #1307 2020-04-14 14:13:54 +02:00
Ozzieisaacs 4749eccfa5 Added fix for python2 regex
Fix for python2 attributeError instead of TypeError on login with wrong openLDAP setting
Added default empty string on LDAPCertificate
Fix ldap as scheme for tls connection
Enabled add user on LDAP Authentication
LDAP config port is now number input
Added header for user import config
Added python ldap version to about section
Fix: It's no longer possible to login via fallback password as long as LDAP server is available
Fix: TypeError on bind is now catched and transformed to error message
Update Readme
Fixes for ldap
2020-04-14 10:58:32 +02:00
Michael Shavit 41a3623fcc [Kobo] Add Shelf/Collection support.
Implements the v1/library/tags set of endpoints to sync CalibreWeb
shelves with the Kobo device.
Drive-by: Refactors shelf.py to consolidate how user permissions are checked.
Drive-by: Fix issue with the sync endpoint that arrises when a book is
hard-deleted.
2020-04-12 22:29:20 -04:00
Marcel 296ac203d4 Update messages.po 2020-04-11 12:32:37 +02:00
xcffl 0c9436ca82 Regenerate translation files 2020-04-11 00:06:56 +08:00
xcffl 70c9dd1b95 Fix opds format errors & extend info 2020-04-11 00:06:50 +08:00
xcffl 80753b1115 Add vscode configs into .gitignore. 2020-04-08 23:01:57 +08:00
Ozzieisaacs a194216568 Merge remote-tracking branch 'origin/master' 2020-04-05 19:10:27 +02:00
Ozzieisaacs 8bee424cc0 Merge remote-tracking branch 'ldap/master'
# Conflicts:
#	cps/admin.py
#	cps/templates/admin.html
#	cps/templates/config_edit.html
#	cps/web.py
2020-04-05 17:31:41 +02:00
Ozzie Isaacs 25ab3cabfe Add question for logfile to Issue Template 2020-04-04 18:28:43 +02:00
Ozzieisaacs 587174b771 Fix #1272 2020-04-04 08:53:22 +02:00
Ozzieisaacs 3e1c34efe6 Merge remote-tracking branch 'fix/fix_covers' into Develop 2020-04-04 06:57:54 +02:00
Ozzieisaacs 5864637f1c Merge branch 'master' into Develop 2020-04-04 06:22:17 +02:00
Ozzieisaacs ec6b346ca1 Update Translation 2020-04-03 19:50:29 +02:00
Ozzieisaacs e99f5bcced Merge remote-tracking branch 'english/master' 2020-04-03 19:46:26 +02:00
Ozzieisaacs b89309ab82 Merge remote-tracking branch 'po/master' 2020-04-03 19:27:10 +02:00
OzzieIsaacs 2d230ec96a Updated Teststatus 2020-04-02 21:13:43 +02:00
Ozzieisaacs 4550333f1e Added redirect after create shelf 2020-04-02 20:19:05 +02:00
Ozzieisaacs 3ba610eb64 Revert accidentally committed changes in opds feed 2020-04-02 19:07:41 +02:00
Ozzieisaacs 2436c6a118 Merge remote-tracking branch 'caliblur/master' 2020-04-02 18:27:21 +02:00
Ozzieisaacs 4de80b26c1 Merge remote-tracking branch 'public_shelf/calibre-web_645'
# Conflicts:
#	cps/templates/feed.xml
#	cps/templates/layout.html
2020-04-02 18:24:32 +02:00
Ozzieisaacs bab14a1fbf Update shelfs handling, bugfix changed updater 2020-04-02 18:23:24 +02:00
Ozzieisaacs 0c27ff11b9 Update Updater 2020-04-01 18:45:16 +02:00
BeckyDTP 734e2ffbb2 Updated Polish translations 2020-03-31 21:53:13 +02:00
hexeth da42c51af2 Updated to exclude new modules 2020-03-31 08:42:08 -07:00
Michael Shavit 7cb6801241 Fix issue with cover images. 2020-03-30 02:17:08 -04:00
Ozzieisaacs f6c04b9b84 Merge branch 'master' into Develop 2020-03-29 16:48:56 +02:00
Ozzieisaacs 4eacb21259 Update updater
Update change logfile
code cosmetics js files
2020-03-29 16:44:24 +02:00
Ozzie Isaacs 6d1a3ccdcc Improve logger for windows 2020-03-28 07:13:51 +01:00
Josh O'Brien c870f6e87d English Language V2 2020-03-19 13:10:10 +11:00
Ozzieisaacs 6d907094d7 Update Readme 2020-03-14 11:22:27 +01:00
Ozzieisaacs 6643f0d1e0 Merge remote-tracking branch 'nl/master'
# Conflicts:
#	cps/translations/nl/LC_MESSAGES/messages.mo
#	cps/translations/nl/LC_MESSAGES/messages.po
2020-03-14 10:45:58 +01:00
Ozzieisaacs 20b07e0752 Merge remote-tracking branch 'sv/master'
# Conflicts:
#	cps/translations/sv/LC_MESSAGES/messages.mo
#	cps/translations/sv/LC_MESSAGES/messages.po
2020-03-14 10:39:27 +01:00
Ozzieisaacs 092423adc7 Merge remote-tracking branch 'sv/master'
# Conflicts:
#	cps/translations/sv/LC_MESSAGES/messages.mo
#	cps/translations/sv/LC_MESSAGES/messages.po
2020-03-14 10:38:50 +01:00
Ozzieisaacs a50ca1a85f Fix #1259 2020-03-14 10:35:58 +01:00
Jony 02199c8c1d Update messages.po (Swedish)
Update Swedish translation
2020-03-14 09:32:33 +01:00
Jony c166c92685 Merge pull request #3 from janeczku/master
Update fork
2020-03-14 09:31:29 +01:00
Ozzieisaacs f243515261 Fix #950 (sorting order in comics) 2020-03-14 07:15:15 +01:00
Ozzieisaacs 98181fe21c Merge remote-tracking branch 'fix/kobo_book_delete' into Develop 2020-03-13 17:00:38 +01:00
Unknown a26ce8d8b5 Updated & corrected Dutch translations
Un-wonky-fied some literal translations, corrected contexts an respected punctuations, whitespaces, etc.....

See https://github.com/janeczku/calibre-web/issues/606#issuecomment-595913808
2020-03-13 14:01:42 +01:00
Michael Shavit de0e27c512 Fix #1164 merge. 2020-03-12 20:09:26 -04:00
Ozzieisaacs 09e7d76c6f Merge remote-tracking branch 'kobo_book_delete' into Develop
# Conflicts:
#	cps/kobo.py
#	cps/services/SyncToken.py
#	cps/templates/book_edit.html
#	cps/ub.py
2020-03-12 20:43:39 +01:00
Ozzieisaacs d597e05fa9 Merge remote-tracking branch 'reading_state' into Develop
# Conflicts:
#	cps/kobo.py
2020-03-12 20:35:24 +01:00
Ozzieisaacs 98dc991339 Merge remote-tracking branch 'more_kobo_fixes' into Develop 2020-03-12 20:33:22 +01:00
Ozzieisaacs 1d40434d2b Merge remote-tracking branch 'it/patch-9' 2020-03-12 20:32:08 +01:00
Ozzieisaacs 46b87dc7eb Fix for testability of kobo 2020-03-09 20:25:59 +01:00
ElQuimm fe7c56d269 italian updated version of messages.po
09.03.2020
2020-03-09 09:11:41 +01:00
OzzieIsaacs 5ba4801a79 Updated test result 2020-03-08 08:02:15 +01:00
Michael Shavit ad564e25ca More fixes for Kobo Sync
- Fix incorrect maintenance of the Synctoken timestamps.
- Fix issue where the SyncToken isn't included in the response when
kobo_proxying is disabled.
2020-03-08 01:20:54 -05:00
Ozzieisaacs 404be948d4 Merge remote-tracking branch 'ldap/Develop' into Develop
# Conflicts:
#	cps/admin.py
#	cps/web.py
2020-03-07 13:39:47 +01:00
Ozzieisaacs 8cbc345f36 Merge remote-tracking branch 'identifier/develop/edit_identifiers' into Develop 2020-03-07 13:35:58 +01:00
Ozzieisaacs 89927fd7e9 Merge ComicVine API 2020-03-07 13:33:35 +01:00
Ozzieisaacs 6b4a024234 Merge branch 'master' into Develop 2020-03-07 12:47:38 +01:00
Ozzieisaacs 18794831e0 Chang link to issue template 2020-03-07 11:27:50 +01:00
Ozzieisaacs 3fb851304f Merge remote-tracking branch 'ru/master' 2020-03-07 11:21:10 +01:00
Ozzieisaacs d267338837 Fixes for Kobo sync
Better output on upload cover
Fix for download after access to opds/fileformat
Fix osd search link
Added ratings to opds feed
Change for kobo sync for testing
2020-03-07 11:07:43 +01:00
Michael Shavit 8e1641dac9 Add support for syncing Kobo reading state. 2020-03-01 22:01:05 -05:00
Michael Shavit 57d37ffba8 Add schema support for more reading states to the ReadBook table. 2020-03-01 20:26:42 -05:00
ZIzA 82afa81220 up 2020-02-29 20:41:51 +04:00
ZIzA d730eb8d31 fix 2020-02-29 20:26:52 +04:00
ZIzA 5a219b580f ru update 2020-02-29 19:10:01 +04:00
Ozzieisaacs fb83bfb363 Fix for #1223 (pdf-reader isn't working on chrome, IE, Edge) 2020-02-29 09:55:54 +01:00
Ozzie Isaacs df7d3d18b6 pdf reader 2020-02-29 08:33:09 +01:00
Wanoo a0535aa3db Add ComicVine Api
Fetching Metadata for comicBook with ComicVine API
2020-02-28 16:04:07 +01:00
Ozzieisaacs 202b6121ab Fix for #1227 (Download books not possible with gevent IPV6 connection)
Fix issue where books aren't correctly ordered by Date Added
2020-02-26 20:05:44 +01:00
Ozzieisaacs 4e8b814ec2 Fix download URL for IPV6 2020-02-26 20:04:15 +01:00
Ozzieisaacs 6becca17bf Merge remote-tracking branch 'fix/fix_time' into Develop 2020-02-26 18:09:01 +01:00
Ozzieisaacs c8b64d4162 Update chinese translation Merge remote-tracking branch 'cn/patch-1' 2020-02-26 18:03:26 +01:00
Jeff 0854303710 Update Chinese translation 2020-02-26 19:47:25 +08:00
Michael Shavit cba3e62e71 Fix issue where books weren't correctly ordered by Date added on the
Kobo device.
2020-02-26 01:06:03 -05:00
Ozzieisaacs dcba720e97 Merge remote-tracking branch 'restrict/patch-1' 2020-02-25 16:06:24 +01:00
Rewerson 6c614c06f6 restricted_tags -> denied_tags
Fix for the next error after update:
Traceback (most recent call last):
  File "cps.py", line 34, in <module>
    from cps import create_app
  File "/home/rewerson/lib/web/cps/__init__.py", line 68, in <module>
    config = config_sql.load_configuration(ub.session)
  File "/home/rewerson/lib/web/cps/config_sql.py", line 348, in load_configuration
    update({"restricted_tags": conf.config_mature_content_tags}, synchronize_session=False)
  File "/home/rewerson/.local/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 3862, in update
    update_op.exec_()
  File "/home/rewerson/.local/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 1693, in exec_
    self._do_exec()
  File "/home/rewerson/.local/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 1874, in _do_exec
    values = self._resolved_values
  File "/home/rewerson/.local/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 1840, in _resolved_values
    desc = _entity_descriptor(self.mapper, k)
  File "/home/rewerson/.local/lib/python2.7/site-packages/sqlalchemy/orm/base.py", line 402, in _entity_descriptor
    "Entity '%s' has no property '%s'" % (description, key)
sqlalchemy.exc.InvalidRequestError: Entity '<class 'cps.ub.User'>' has no property 'restricted_tags'
2020-02-25 16:30:51 +03:00
Ozzieisaacs 9a812f11e7 Merge remote-tracking branch 'it/patch-8'
# Conflicts:
#	cps/translations/it/LC_MESSAGES/messages.po
2020-02-24 19:40:10 +01:00
Ozzieisaacs 917132fe26 Update Translation (german and russian)
Merge remote-tracking branch 'it/patch-8'

# Conflicts:
#	cps/translations/it/LC_MESSAGES/messages.po
2020-02-24 19:38:12 +01:00
Ozzieisaacs 187ca5dc8f Fix #1221 2020-02-24 19:08:54 +01:00
Ozzieisaacs 7d795771d3 Fix #1221 2020-02-24 19:02:38 +01:00
ElQuimm e51bc4ea78 Update italian message.po
Thank you for your job :-)
2020-02-23 21:23:00 +01:00
Ozzieisaacs 2dc3235d4f Merge branch 'master' into Develop 2020-02-23 20:08:37 +01:00
Ozzieisaacs 040bb4a5a8 Merge remote-tracking branch 'origin/master' 2020-02-23 20:07:51 +01:00
Ozzieisaacs fc4436f091 Update Translation 2020-02-23 20:07:21 +01:00
Ozzie Isaacs 5dbdef25d3 Windows compatibility 2020-02-23 17:24:25 +01:00
Ozzie Isaacs b9f3ac2eea Windows compatability 2020-02-23 17:23:38 +01:00
Ozzieisaacs 9fc0c3b3de Merge remote-tracking branch 'English_texts/master'
# Conflicts:
#	cps/templates/config_edit.html
#	cps/templates/config_view_edit.html
#	cps/templates/layout.html
#	cps/templates/search_form.html
#	cps/templates/user_edit.html
2020-02-23 13:27:44 +01:00
Ozzieisaacs 4f81184da0 Update translation 2020-02-23 13:22:57 +01:00
Ozzieisaacs 8223561844 Merge branch 'master' into Develop 2020-02-23 13:21:11 +01:00
Ozzieisaacs 32a6beae65 Merge remote-tracking branch 'fix/kobo'
# Conflicts:
#	cps/kobo.py
2020-02-23 13:17:59 +01:00
Ozzieisaacs 146068c936 Merge branch 'Develop'
# Conflicts:
#	cps/db.py
#	cps/templates/user_edit.html
2020-02-23 13:15:30 +01:00
Ozzieisaacs 3b8c5ef21a Merge remote-tracking branch 'get_metadata/patch-1'
Fix #1210 Load multiple authors correct via get metadata
2020-02-23 13:14:12 +01:00
Ozzieisaacs e60ef8fc97 Calibre-web version visible in about section
Update Calibre-web Version info
2020-02-23 12:40:11 +01:00
Ozzieisaacs 24d755b123 Update translations
Merge remote-tracking branch 'it/patch-7'
2020-02-23 10:55:56 +01:00
Ozzieisaacs 7c89f0b5b9 Merge with branch develop
Release version
2020-02-23 09:08:01 +01:00
Ozzieisaacs 134a10f56c Merge remote-tracking branch 'origin/Develop' into Develop
# Conflicts:
#	cps/updater.py
2020-02-23 08:58:47 +01:00
Ozzieisaacs 3a70c86f49 Update updater to handle venv folders 2020-02-23 08:58:13 +01:00
Ozzieisaacs ac431bbc4a Limit requirements 2020-02-23 08:58:13 +01:00
OzzieIsaacs 371097eb4d Update Updater 2020-02-23 07:54:58 +01:00
OzzieIsaacs 6346059698 Changed optional-requirements 2020-02-23 06:53:17 +01:00
Ozzieisaacs 372c284ad4 Merge branch 'master' into Develop 2020-02-22 16:24:06 +01:00
Ozzie Isaacs 264ccdbf6d Update bug_report.md 2020-02-22 13:36:31 +01:00
Ozzie Isaacs b95150ff11 Update bug report 2020-02-22 13:33:21 +01:00
Ozzie Isaacs 81fc04f24d Update bug_report.md 2020-02-22 13:29:36 +01:00
Ozzie Isaacs 3d4ebddb04 Update issue templates 2020-02-22 13:27:56 +01:00
Ozzie Isaacs de299e0d6a Update issue templates 2020-02-22 13:25:31 +01:00
Johnny A. dos Santos 29cb8bfec4 Fix #1210 2020-02-21 18:25:02 -03:00
Ozzieisaacs b7f3e00fbf Update updater to handle venv folders 2020-02-21 18:04:26 +01:00
Ozzieisaacs 27a18d60a7 Merge remote-tracking branch 'fix/robust_proxy' into Develop
# Conflicts:
#	cps/kobo.py
2020-02-19 18:20:03 +01:00
Ozzieisaacs de8f6d3e8d Limit requirements 2020-02-19 18:15:40 +01:00
Ozzieisaacs 6893635251 Handle kobo auth request
Handle access from localhost for kobo
2020-02-18 20:36:32 +01:00
ElQuimm 94a38a3b47 udate message.po in italian
Hello, 
updated one more time the translation :-)
I think the string "Author list" is missing.
Thank you!
Quimm
2020-02-17 21:03:33 +01:00
Michael Shavit 7d99e21d0d Merge remote-tracking branch 'original/Develop' into kobo_book_delete 2020-02-17 14:49:42 -05:00
Michael Shavit df3eb40e3c Make KoboStore proxying more robust.
* Add a timeout to prevent hanging when the KoboStore isn't reachable.
* Add back a the dummy auth implementation for when proxying is
disabled.
* Return the dummy auth response as a fallback when failing to contact
the KoboStore.
* Don't contact the KoboStore during the /sync API call when proxying is
disabled.
2020-02-17 14:37:02 -05:00
Ozzieisaacs ba6b5f8fd1 Merge branch 'master' into Develop 2020-02-17 18:42:53 +01:00
Josh O'Brien 8f518993a4 Merge branch 'master' into master 2020-02-18 00:21:59 +11:00
Niktia Pchelin dac48a2610 #645 - changes create_shelf and edit_shelf to allow user create public and private shelves with the same name 2020-02-16 23:38:35 -05:00
Niktia Pchelin 7c0d10da79 #645 - displays '(public)' next to user's public shelves in OPDS feed 2020-02-16 22:47:32 -05:00
Niktia Pchelin d77b52af96 #645 - displays '(public)' next to public shelves created by the user 2020-02-16 22:24:00 -05:00
Ozzieisaacs 29f6463ed9 Fix #1149 (Prevent invalid file extensions on Kindle Paperwhite download) 2020-02-16 19:55:05 +01:00
Ozzieisaacs ed0bdbf31d Fix for #1199 (missing referrer) 2020-02-16 19:39:32 +01:00
Ozzieisaacs b152d3e06d Merge remote-tracking branch 'auth_token/fix/reverse-proxy-authentication-with-bearer-token' 2020-02-16 19:24:28 +01:00
Ozzieisaacs 16cd57fe55 Merge remote-tracking branch 'cover_layout/fix-pull-left-cover-layout' 2020-02-16 19:19:10 +01:00
Ozzieisaacs 3f578122a3 Merge remote-tracking branch 'it/patch-6' 2020-02-16 18:56:14 +01:00
Ozzieisaacs 51a27322be Merge remote-tracking branch 'cn/master' 2020-02-16 18:53:27 +01:00
OzzieIsaacs 050feed5dc Updated test result 2020-02-16 16:53:54 +01:00
Ozzieisaacs e3ddc16657 Fix opds login not working anymore (due to kobo sync protocol) 2020-02-16 14:25:15 +01:00
André Frimberger 33cdf20cd5 Remove trailing whitespaces 2020-02-16 10:25:01 +01:00
Ozzieisaacs 317e59df4b Bugfixes from tests 2020-02-15 16:02:31 +01:00
Ozzieisaacs a9a6f5b97e Renamed restrict -> deny 2020-02-15 10:21:45 +01:00
Ozzieisaacs 8b1444ebc2 Improved testability
Less content exposed with restricted content
Tornado restart and stop working with systemd
2020-02-15 09:08:39 +01:00
Kyos 509071949a Moved style to css file 2020-02-13 21:05:24 +01:00
ElQuimm 697d857549 message.po italian 2020-02-13 11:29:26 +01:00
Ozzieisaacs 2ea45b1fdc Removed shebang on non executable scripts 2020-02-10 18:37:47 +01:00
Ozzieisaacs 726595e117 Update for testability 2020-02-10 18:34:10 +01:00
Kyos 1666e32aaf Fixes cover layouts in user edit page 2020-02-09 22:31:08 +01:00
Kyos 6a69bbe4b5 Fixes cover layouts in user edit page 2020-02-09 22:20:52 +01:00
André Frimberger 7a608b4fb0 fix binascii.Error with reverse proxy bearer token
When an authenticating reverse proxy (e.g. Keycloak Gatekeeper) adds a Bearer token in the Authorization header, every request fails with HTTP status code 500.  The corresponding error in the logs is: binascii.Error: Incorrect padding. 
Despite "reverse_proxy_header_login" is enabled, calibre-web tries first to base64decode the bearer token and fails. This patch just reverses the order in which the authentication methods  are checked.
2020-02-09 17:21:22 +01:00
Ozzieisaacs 814ad87a42 Updated pdf viewer to 2.2.228 (#1184) 2020-02-09 17:02:47 +01:00
Ozzieisaacs 3e4b5e23fa Updated pdf viewer to 2.2.228 (#1184) 2020-02-09 17:02:28 +01:00
Ozzieisaacs ab24ed8088 Handle sorting of hidden books #1123
Prevent downloading of covers from hidden books
2020-02-09 14:31:42 +01:00
Ozzieisaacs 50ba2e329a Merge branch 'master' into Develop
# Conflicts:
#	cps/shelf.py
2020-02-09 07:00:04 +01:00
Ozzie Isaacs c1e2a98f46 Merge remote-tracking branch 'origin/master' 2020-02-08 14:40:28 +01:00
Ozzie Isaacs e04aa80fd6 Fix #1181 and Fix #1182 handle removed "is_xhr" on werkzeug version 1.0.0 2020-02-08 14:40:13 +01:00
Ozzie Isaacs 482e977af4 Clarified installation procedure 2020-02-08 13:27:51 +01:00
Ozzie Isaacs 2535bbbcf1 Fix #1180 working on windows with tornado and python <3.8 again 2020-02-08 13:24:01 +01:00
Ozzieisaacs 6698773d81 Improved testability 2020-02-08 11:33:31 +01:00
xcffl aefaf47f4c Update Simplified Chinese translation 2020-02-03 17:26:17 +08:00
Josh O'Brien 9b49125776 English Language Updates
Changes to provide consistency for English language.
2020-02-03 14:22:00 +11:00
Ozzieisaacs b33a2ac90d Fix #1170 Auth-digest Header no longer crashs calibre-web
Fix #1161 Shelfs are reordering again
Update Sortable.js to 2.10.1
Code cosmetics
2020-02-02 07:47:27 +01:00
Ozzieisaacs f67953c447 Update translation
Merge remote-tracking branch 'cz/patch-11'
2020-02-01 15:03:03 +01:00
Ozzieisaacs 981632f599 Merge remote-tracking branch 'it/patch-5' 2020-02-01 15:02:10 +01:00
Ozzieisaacs a6c453d826 Merge remote-tracking branch 'pl/pl_translate_update' 2020-02-01 15:01:58 +01:00
Ozzieisaacs 4087e685f4 Fix auto detection of locale 2020-02-01 13:40:29 +01:00
Ozzieisaacs 5255085de1 Fix auto detection of locale 2020-02-01 13:38:11 +01:00
ElQuimm 9247ded710 Update - italian version of message.po 2020-01-29 21:41:54 +01:00
Jerzy Piątek 0bb0cbaef0 Updated polish translations 2020-01-28 22:28:58 +01:00
Lukáš Heroudek 0f7d272e13 Update messages.po 2020-01-28 10:03:40 +00:00
Ozzieisaacs 00dafe3121 Fix restrict columns 2020-01-27 20:34:36 +01:00
Ozzieisaacs e44494aad0 Kobo sync enable/disable working 2020-01-27 20:32:37 +01:00
Ozzieisaacs 4ab3dc2599 Merge branch 'master' into Develop
# Conflicts:
#	cps/admin.py
2020-01-27 18:20:50 +01:00
Ozzieisaacs acfad7a982 Update Italian translation 2020-01-27 18:17:17 +01:00
Ozzieisaacs b29b5b7ac1 Merge remote-tracking branch 'github/fix-admin-view-log-file' 2020-01-27 18:14:16 +01:00
ElQuimm 7803ffb995 Update italian message.po
Thank you.
2020-01-27 15:07:00 +01:00
Ghighi Eftimie fc79cdfaa2 fix mismerge 2020-01-27 15:24:11 +02:00
Michael Shavit f9dbc6bc78 Clean-up book from ArchivedBook on hard-delete.
This change also adds a warning to the hard-delete prompt that deleted
books should first be archived if the Kobo Sync feature is enabled.
An alternative would be to keep a permanent record of hard-deleted
book.
2020-01-26 16:20:10 -05:00
Michael Shavit dc7aaae235 Now that CalibreWeb delete requests are respected, we can forward
them to the KoboStore for books that aren't in Calibre.

Note: There's still an edge case where a book is removed from Calibre
without first being archived, in which case the delete call will fail.
2020-01-26 16:01:27 -05:00
Michael Shavit 9804a98af8 Merge remote-tracking branch 'original/Develop' into kobo_book_delete 2020-01-26 15:58:57 -05:00
Ghighi Eftimie 647e954e8a fix admin view of error log 2020-01-26 20:53:03 +02:00
Ozzieisaacs 004d9118bc Merge branch 'master' into Develop 2020-01-26 19:37:14 +01:00
Ozzieisaacs 594c8aad91 Update Version Info 2020-01-26 19:36:15 +01:00
Ozzieisaacs 542a0008c9 Merge remote-tracking branch 'ru/master' 2020-01-26 19:33:37 +01:00
Ozzieisaacs 24f7918aa4 Merge remote-tracking branch 'it/patch-4' 2020-01-26 19:31:52 +01:00
Ozzieisaacs 2eec329bdf Merge branch 'master' into Develop
# Conflicts:
#	cps/templates/user_edit.html
#	optional-requirements.txt
2020-01-26 19:29:36 +01:00
Ozzieisaacs 0411d4a8c9 Added 2 new kobo settings: Enable Kobo Sync (currently not working) and proxy Requests to Kobo
Added fix for kobo reader generating requests without right port number, causing url_for not working correct
2020-01-26 16:52:40 +01:00
Ozzieisaacs a986faea56 Merge remote-tracking branch 'fix/fix_creation_time' into Develop 2020-01-26 16:50:38 +01:00
Ozzieisaacs ad71d0a03f Merge remote-tracking branch 'kobo_/develop/fix-new-user' into Develop 2020-01-26 14:43:44 +01:00
Ozzieisaacs 0955c6d6fb Update Kobo 2020-01-26 14:42:53 +01:00
Michael Shavit d30b44ee0f Minor formatting changes per codacy review. 2020-01-26 01:12:52 -05:00
Michael Shavit a6f4db0f25 Fix bug where last_created is incorectly set in the SyncToken. 2020-01-26 00:32:05 -05:00
Michael Shavit 4547c328bc Delete/Restore book from Kobo device upon (un)archiving of a book in the web UI. 2020-01-26 00:29:36 -05:00
Michael Shavit 5027aeb3a0 Fix bug where last_created is incorectly set in the SyncToken. 2020-01-26 00:29:36 -05:00
Michael Shavit c0239a659c Add UI support for archived books.
Archived books will no longer appear in any book lists or searches, and
may only be restored from the Archive view.
2020-01-26 00:27:54 -05:00
Michael Shavit e404da4192 Add support for book 'deletion' (i.e archiving) from a Kobo device. 2020-01-26 00:16:55 -05:00
Simon Latapie 69fa7d0091 new user: disable kobo token on new user page
kobo token generator function is related to a user id, which is not
present on new user page.
Disable the Kobo token part when creating a new user.
2020-01-25 22:41:55 +01:00
ZIzA e1d6aec682 quick fix
sorry
2020-01-21 23:06:22 +04:00
ZIzA 155795a18e Russian language update 2020-01-21 22:29:23 +04:00
ZIzA 8c4052e884 Update README.md 2020-01-21 22:22:10 +04:00
Simon Latapie 9b119fa724 edit book: manage identifiers 2020-01-20 22:26:43 +01:00
ElQuimm a17c1c063e Update messages.po
:-)
Thank you
2020-01-20 22:05:11 +01:00
Ozzieisaacs cd9bb56db5 Cleanup Kobo integration 2020-01-20 06:14:53 +01:00
Сергей 8150f934fd Update README.md 2020-01-20 01:48:57 +04:00
Сергей 4c8f3f7bae Add files via upload 2020-01-20 01:46:41 +04:00
Сергей ab5f176f58 Update README.md 2020-01-20 01:45:37 +04:00
Ozzieisaacs 3e404101bd Merge remote-tracking branch 'cc/develop/custom_float' into Develop 2020-01-15 17:58:57 +01:00
Ozzieisaacs 65105d9dbe Merge remote-tracking branch 'cc/develop/custom_float' into Develop 2020-01-15 17:58:23 +01:00
Ozzieisaacs 3a4d351a57 Merge remote-tracking branch 'develop/calibur-fix-colon' into Develop 2020-01-15 17:57:00 +01:00
Ozzieisaacs 973f555544 Merge branch 'master' into Develop
(updated tinymce and bootstrap datepicker)
2020-01-13 18:39:25 +01:00
Simon Latapie 56964a890b caliBlur theme: fix custom column display that contain colons 2020-01-12 14:26:07 +01:00
Simon Latapie cef41661dd custom columns: add Float type support
- remove float datatype from cc_exceptions
- handle float datatype like int or bool
- change edit template to modify input step (0.01 precision, like
  in calibre UI)
2020-01-12 14:16:08 +01:00
Ozzieisaacs 79a9ef4859 Make Kobo optional
move jsonschema dependency to optional-requirements.txt
Added version of jsonschema to about section
Added additional column to RemoteAuthToken table
Update configuration of Kobo sync protocol
2020-01-12 13:42:39 +01:00
Ozzieisaacs 2798dd5916 Merge remote-tracking branch 'fix/kobo' into Develop
# Conflicts:
#	cps/db.py
#	cps/web.py
2020-01-11 18:35:04 +01:00
Ozzieisaacs 8143c16c14 Added texts for allowed denied tags in configuration
Removed domain only restriction (now it's also possible to allow/deny name parts of users email)
Fix no. of displayed books in unread books section, with applied restrictions
2020-01-11 13:28:09 +01:00
Ozzieisaacs 42435ab34a Improved testablility 2020-01-08 19:52:05 +01:00
Ozzieisaacs 661ed17d23 Handle stream output in logviewer
Add migration of global matrue content settings to user based settings
Fix Caliblur Theme (Publisherslist)
Allow/deny list/edit/add/delete of tags and custom columns
Colored table background for tags restriction
2020-01-07 20:26:59 +01:00
Ozzieisaacs 218e35e3aa Merge remote-tracking branch 'origin/Develop' into Develop 2020-01-05 16:20:46 +01:00
Ozzieisaacs cabad83418 moved language selection back, default value for restriction columns
Merge branch 'master' into Develop

# Conflicts:
#	cps/editbooks.py
2020-01-05 16:20:36 +01:00
Ozzieisaacs 87c415830f Merge branch 'master' into Develop
# Conflicts:
#	cps/editbooks.py
2020-01-05 13:37:45 +01:00
Ozzieisaacs 48495f0d66 Merge branch 'master' into Develop
# Conflicts:
#	cps/helper.py
#	cps/templates/user_edit.html
#	cps/web.py
2020-01-04 07:57:54 +01:00
Ozzieisaacs d9adb4fc94 Improvements for limit tags 2020-01-01 17:30:08 +01:00
Ozzieisaacs 1da4efec86 Merge branch 'master' into Develop 2019-12-31 10:52:45 +01:00
Ozzieisaacs 1c630eb604 fixes from tests 2019-12-30 15:16:09 +01:00
Ozzieisaacs 1c18a788f4 Merge remote-tracking branch 'tags_restrict/master' into Develop 2019-12-30 15:15:07 +01:00
Ozzieisaacs 5887f0fe6b Fix for #768 (If email server is configured, admins can send user passwords, also domains can be denied from registration) 2019-12-29 13:54:52 +01:00
ground7 b782489a8c ldap opds download bugged 2019-12-28 21:52:26 -07:00
Ozzieisaacs 01381488f4 Fixes from tests 2019-12-28 16:18:21 +01:00
ground7 6555d5869f attempt regular login if ldap login fails as fallback 2019-12-27 23:45:42 -07:00
ground7 54c4f40188 added LDAP import
update defaults
2019-12-27 23:12:18 -07:00
Ozzieisaacs 62e8bee2a8 Fix response opds with read/unread
changed db_reconnect
Changed output for error 500 (now including error message)
Fix in task queue after 20 messages
2019-12-24 13:02:53 +01:00
Michael Shavit 9ec3ddd492 Fix the HandleCoverImage endpoint so that it requires login, and doesn't
take unused parameters.
2019-12-22 16:59:00 -05:00
Michael Shavit d81dbb13e4 Support Epub downloads 2019-12-22 16:59:00 -05:00
Michael Shavit c238367b64 Reload database on every call to v1/library/sync. This fixes an issue where side-loaded books appear in the sync response with no download urls. 2019-12-22 16:59:00 -05:00
Michael Shavit cdcb8a50d1 Fix /reconnect endpoint, which was broken by https://github.com/janeczku/calibre-web/commit/006e596c7221729c9a37b79037fa72bb88506492 2019-12-22 16:58:55 -05:00
Michael Shavit b831b9d6b2 Integrate with the official Kobo store endpoint so that no
functionanility is lost by overriding the api_endpoint setting.

Requests are either:
 * Redirected to the Kobo Store
 * Proxied to the Kobo Store
 * Proxied to the Kobo Store and merged with results from CalibreWeb.
2019-12-22 13:40:49 -05:00
Ozzieisaacs bbe91f439a Merge from master 2019-12-22 15:28:43 +01:00
Ozzieisaacs 288944db2c Merge remote-tracking branch 'kobo_sync/kobo' into Develop
# Conflicts:
#	cps.py
#	cps/kobo.py
#	cps/kobo_auth.py
#	cps/ub.py
2019-12-20 19:24:31 +01:00
Ozzieisaacs f2c07d8f81 Update Kobo sync 2019-12-20 19:17:08 +01:00
Michael Shavit d6a9746824 Add a filter to the Sync request endpoint to ignore books that don't
have any formats supported by the device.
2019-12-20 01:28:53 -05:00
Michael Shavit f84274f1c5 git add missing generate_kobo_auth_url.html 2019-12-20 01:08:15 -05:00
Michael Shavit 2118d920f5 Formatter. 2019-12-20 01:04:12 -05:00
Michael Shavit 207004beff Remove config_server_url setting. 2019-12-20 01:02:49 -05:00
Michael Shavit 27d084ce39 Remove the KoboUserKey-based Authentication.
Instead, the user generates the api_endpoint url to set on their device
by visiting http://.../kobo_auth/generate_auth_token.
The generated url will contain a RemoteAuthorizationToken that will be
included on all subsequent requests from the device to the kobo/
endpoints. (In contrast, the device is authenticated using a session cookie on
requests to the download endpoint).

Also use Flask.url_for to generate download urls.
2019-12-20 00:55:53 -05:00
Ozzieisaacs f705889c23 Inital Kobo 2019-12-17 20:28:20 +01:00
Michael Shavit 040d7d9ae3 Remove unused import and fix python3 compatibility, as per Ozielsaacs comments. 2019-12-11 00:12:35 -05:00
Michael Shavit f9b1e84704 Remove backblaze leftovers 2019-12-10 23:57:53 -05:00
Michael Shavit 55c0bb6d34 Set the "Size" attribute for Kobo download_urls, and refactor the code
to eventually allow formats other than KEPUB.
2019-12-08 17:28:25 -05:00
Michael Shavit 2b55b9b250 Use the login_user Cookie to authorize download requests instead of
passing the UserKey over url params.
2019-12-08 15:33:57 -05:00
Michael Shavit fffa2d5a1b Support passing the Auth token as a url param.
This is required to support ebook downloads which the Kobo device emits
without any auth headers.

* Also some other small bug fixes discovered during device testing.
2019-12-07 22:55:07 -05:00
Michael Shavit 0926ae530c Remove custom_column usages from an earlier commit. 2019-12-07 21:08:53 -05:00
Michael Shavit 0b709f7dfb Merge branch 'master' into kobo 2019-12-07 20:04:54 -05:00
Michael Shavit b5da2c4199 Clean-up: Fix import order in kobo.py, and other minor formatting
changes.
2019-12-07 19:54:49 -05:00
Michael Shavit 9ede01f130 * Add a UserKeyToken to the User table for Kobo authorization.
* Add proper authorization checks on the new Kobo endpoints.

Important Note: As a side-effect, all CalibreWeb API calls can be
authorized using this token (i.e without a username&password).
2019-12-07 19:54:44 -05:00
Michael Shavit 55b54de6a0 Add simple get_download_url implementation to replace the backblaze-backed implementation 2019-12-05 19:06:39 -05:00
Michael Shavit 5357867103 Add initial support for Kobo device Sync endpoint.
- Supports /v1/library/sync call to get list of books
- Supports /v1/library/metadata call to get metadata for a given book
  + Assumes books are stored on Backblaze for metadata call
- Changes to helper.py so that we can return no cover instead of a blank
image.
2019-12-05 01:56:59 -05:00
Krakinou 11c8b47c39 Merge remote-tracking branch 'upstream/Develop' into Develop
Conflicts:
	cps/web.py
2019-07-01 22:58:11 +02:00
Krakinou 00a29f3d88 Check for change before encoding 2019-07-01 21:45:35 +02:00
Krakinou e5b9da5201 Error management 2019-07-01 21:44:58 +02:00
Krakinou 3d0beba261 Base64 2019-06-17 23:47:35 +02:00
Krakinou 147947662c Base64 2019-06-17 23:46:38 +02:00
tomjmul 8e8486497f Restirct a user to a set of tags 2019-03-19 13:53:41 +00:00
191 changed files with 37477 additions and 35059 deletions
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug/Problem report
about: Create a report to help us improve Calibre-Web
title: ''
labels: ''
assignees: ''
---
**Describe the bug/problem**
A clear and concise description of what the bug is. If you are asking for support, please check our [Wiki](https://github.com/janeczku/calibre-web/wiki) if your question is already answered there.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Logfile**
Add content of calibre-web.log file or the relevant error, try to reproduce your problem with "debug" log-level to get more output.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. Windows 10/raspian]
- Python version [e.g. python2.7]
- Calibre-Web version [e.g. 0.6.5 or master@16.02.20, 19:55 ]:
- Docker container [ None/Technosoft2000/Linuxuser]:
- Special Hardware [e.g. Rasperry Pi Zero]
- Browser [e.g. chrome, safari]
**Additional context**
Add any other context about the problem here. [e.g. access via reverse proxy]
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for Calibre-Web
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+1 -3
View File
@@ -21,14 +21,12 @@ vendor/
# calibre-web
*.db
*.log
config.ini
cps/static/[0-9]*
.idea/
*.bak
*.log.*
tags
settings.yaml
gdrive_credentials
client_secrets.json
+49
View File
@@ -0,0 +1,49 @@
## How to contribute to Calibre-Web
First of all, we would like to thank you for reading this text. we are happy you are willing to contribute to Calibre-Web
### **General**
**Communication language** is english. Google translated texts are not as bad as you might think, they are usually understandable, so don't worry if you generate your post that way.
**Calibre-Web** is not **Calibre**. If you are having a question regarding Calibre please post this at their [repository](https://github.com/kovidgoyal/calibre).
**Docker-Containers** of Calibre-Web are maintained by different persons than the people who drive Calibre-Web. If we come to the conclusion during our analysis that the problem is related to Docker, we might ask you to open a new issue at the reprository of the Docker Container.
If you are having **Basic Installation Problems** with python or it's dependencies, please consider using your favorite search engine to find a solution. In case you can't find a solution, we are happy to help you.
We can offer only very limited support regarding configuration of **Reverse-Proxy Installations**, **OPDS-Reader** or other programs in combination with Calibre-Web.
### **Translation**
Some of the user languages in Calibre-Web having missing translations. We are happy to add the missing texts if you are translate them. Create a Pull Request, create an issue with the .po file attached, or write an email to "ozzie.fernandez.isaacs@googlemail.com" with attached translation file. To display all book languages in your native language an additional file is used (iso_language_names.py). The content of this file is autogenerated with the corresponding translations of Calibre, please do not edit this file on your own.
### **Documentation**
The Calibre-Web documentation is hosted in the Github [Wiki](https://github.com/janeczku/calibre-web/wiki). The Wiki is open to everybody, if you find a problem, feel free to correct it. If information is missing, you are welcome to add it. The content will be reviewed time by time. Please try to be consitent with the form with the other Wiki pages (e.g. the project name is Calibre-Web with 2 capital letters and a dash in between).
### **Reporting a bug**
Do not open up a GitHub issue if the bug is a **security vulnerability** in Calibre-Web. Please write intead an email to "ozzie.fernandez.isaacs@googlemail.com".
Ensure the ***bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=bug_report.md&title=). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you providing the right information. Deleting the form and just pasting the stack trace doesn't speed up fixing the problem. If your issue could be resolved, consider closing the issue.
### **Feature Request**
If there is afeature missing in Calibre-Web and you can't find a feature request in the [Issues](https://github.com/janeczku/calibre-web/issues) section, you could create a [feature request](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=feature_request.md&title=).
We will not extend Calibre-Web with any more login abilitys or add further files storages, or file syncing ability. Furthermore Calibre-Web is made for home usage for company inhouse usage, so requests regarding any sorts of social interaction capability, payment routines, search engine or web site analytics integration will not be implemeted.
### **Contributing code to Calibre-Web**
Open a new GitHub pull request with the patch. Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consits 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 [seperate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unittest and performs real system tests with selenium, 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.
+7 -5
View File
@@ -12,7 +12,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, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, ukrainian
- User Interface in czech, dutch, english, finnish, french, german, hungarian, italian, japanese, khmer, polish, russian, simplified 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)
@@ -21,16 +21,18 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- Restrict eBook download to logged-in users
- 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)
- Upload new books in many formats
- Support for Calibre custom columns
- Ability to hide content based on categories for certain users
- 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
- Self-update capability
- "Magic Link" login to make it easy to log on eReaders
- Login via LDAP, google/github oauth and via proxy authentication
## Quick start
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt`.
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x) or `pip install --target vendor -r requirements.txt` (python2.7).
2. Execute the command: `python cps.py` (or `nohup python 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\
+11
View File
@@ -41,6 +41,14 @@ from cps.shelf import shelf
from cps.admin import admi
from cps.gdrive import gdrive
from cps.editbooks import editbook
try:
from cps.kobo import kobo, get_kobo_activated
from cps.kobo_auth import kobo_auth
kobo_available = get_kobo_activated()
except ImportError:
kobo_available = False
try:
from cps.oauth_bb import oauth
oauth_available = True
@@ -58,6 +66,9 @@ def main():
app.register_blueprint(admi)
app.register_blueprint(gdrive)
app.register_blueprint(editbook)
if kobo_available:
app.register_blueprint(kobo)
app.register_blueprint(kobo_auth)
if oauth_available:
app.register_blueprint(oauth)
success = web_server.start()
Executable → Regular
+7 -11
View File
@@ -33,11 +33,10 @@ from flask_login import LoginManager
from flask_babel import Babel
from flask_principal import Principal
from . import logger, cache_buster, cli, config_sql, ub, db, services
from . import config_sql, logger, cache_buster, cli, ub, db
from .reverseproxy import ReverseProxied
from .server import WebServer
mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub')
@@ -62,7 +61,6 @@ lm = LoginManager()
lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous
ub.init_db(cli.settingspath)
# pylint: disable=no-member
config = config_sql.load_configuration(ub.session)
@@ -75,6 +73,7 @@ _BABEL_TRANSLATIONS = set()
log = logger.create()
from . import services
def create_app():
app.wsgi_app = ReverseProxied(app.wsgi_app)
@@ -89,7 +88,7 @@ def create_app():
log.info('Starting Calibre Web...')
Principal(app)
lm.init_app(app)
app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT')
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
web_server.init_app(app, config)
db.setup_db(config)
@@ -116,14 +115,13 @@ def get_locale():
if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale
preferred = set()
preferred = list()
if request.accept_languages:
for x in request.accept_languages.values():
try:
preferred.add(str(LC.parse(x.replace('-', '_'))))
preferred.append(str(LC.parse(x.replace('-', '_'))))
except (UnknownLocaleError, ValueError) as e:
log.warning('Could not parse locale "%s": %s', x, e)
# preferred.append('en')
log.debug('Could not parse locale "%s": %s', x, e)
return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS)
@@ -135,6 +133,4 @@ def get_timezone():
from .updater import Updater
updater_thread = Updater()
__all__ = ['app']
updater_thread.start()
+15 -5
View File
@@ -30,7 +30,7 @@ import babel, pytz, requests, sqlalchemy
import werkzeug, flask, flask_login, flask_principal, jinja2
from flask_babel import gettext as _
from . import db, converter, uploader, server, isoLanguages
from . import db, converter, uploader, server, isoLanguages, constants
from .web import render_title_template
try:
from flask_login import __version__ as flask_loginVersion
@@ -43,14 +43,22 @@ try:
except ImportError:
unidecode_version = _(u'not installed')
try:
from flask_dance import __version__ as flask_danceVersion
except ImportError:
flask_danceVersion = None
from . import services
about = flask.Blueprint('about', __name__)
_VERSIONS = OrderedDict(
Platform = ' '.join(platform.uname()),
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
Python=sys.version,
Calibre_Web=constants.STABLE_VERSION['version'] + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
WebServer=server.VERSION,
Flask=flask.__version__,
Flask_Login=flask_loginVersion,
@@ -65,9 +73,11 @@ _VERSIONS = OrderedDict(
iso639=isoLanguages.__version__,
pytz=pytz.__version__,
Unidecode = unidecode_version,
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else u'not installed',
Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed',
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
)
_VERSIONS.update(uploader.get_versions())
+371 -80
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
@@ -43,21 +42,25 @@ from .helper import speaking_language, check_valid_domain, send_test_mail, reset
from .gdriveutils import is_gdrive_ready, gdrive_support
from .web import admin_required, render_title_template, before_request, unconfigured, login_required_if_no_ano
log = logger.create()
feature_support = {
'ldap': False, # bool(services.ldap),
'goodreads': bool(services.goodreads_support)
'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo)
}
# try:
# import rarfile
# feature_support['rar'] = True
# except ImportError:
# feature_support['rar'] = False
try:
import rarfile
feature_support['rar'] = True
except ImportError:
feature_support['rar'] = False
try:
from .oauth_bb import oauth_check, oauthblueprints
feature_support['oauth'] = True
except ImportError:
except ImportError as err:
log.debug('Cannot import Flask-Dance, login with Oauth will not work: %s', err)
feature_support['oauth'] = False
oauthblueprints = []
oauth_check = {}
@@ -65,7 +68,7 @@ except ImportError:
feature_support['gdrive'] = gdrive_support
admi = Blueprint('admin', __name__)
log = logger.create()
@admi.route("/admin")
@@ -79,12 +82,12 @@ def admin_forbidden():
@admin_required
def shutdown():
task = int(request.args.get("parameter").strip())
showtext = {}
if task in (0, 1): # valid commandos received
# close all database connections
db.dispose()
ub.dispose()
showtext = {}
if task == 0:
showtext['text'] = _(u'Server restarted, please reload page')
else:
@@ -96,9 +99,11 @@ def shutdown():
if task == 2:
log.warning("reconnecting to calibre database")
db.setup_db(config)
return '{}'
showtext['text'] = _(u'Reconnect successful')
return json.dumps(showtext)
abort(404)
showtext['text'] = _(u'Unknown command')
return json.dumps(showtext), 400
@admi.route("/admin/view")
@@ -144,7 +149,10 @@ def configuration():
def view_configuration():
readColumn = db.session.query(db.Custom_Columns)\
.filter(and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all()
restrictColumns= db.session.query(db.Custom_Columns)\
.filter(and_(db.Custom_Columns.datatype == 'text',db.Custom_Columns.mark_for_delete == 0)).all()
return render_title_template("config_view_edit.html", conf=config, readColumns=readColumn,
restrictColumns=restrictColumns,
title=_(u"UI Configuration"), page="uiconfig")
@@ -160,7 +168,7 @@ def update_view_configuration():
_config_string("config_calibre_web_title")
_config_string("config_columns_to_ignore")
_config_string("config_mature_content_tags")
# _config_string("config_mature_content_tags")
reboot_required |= _config_string("config_title_regex")
_config_int("config_read_column")
@@ -168,6 +176,7 @@ def update_view_configuration():
_config_int("config_random_books")
_config_int("config_books_per_page")
_config_int("config_authors_max")
_config_int("config_restricted_column")
if config.config_google_drive_watch_changes_response:
config.config_google_drive_watch_changes_response = json.dumps(config.config_google_drive_watch_changes_response)
@@ -176,8 +185,6 @@ def update_view_configuration():
config.config_default_role &= ~constants.ROLE_ANONYMOUS
config.config_default_show = sum(int(k[5:]) for k in to_save if k.startswith('show_'))
if "Show_mature_content" in to_save:
config.config_default_show |= constants.MATURE_CONTENT
if "Show_detail_random" in to_save:
config.config_default_show |= constants.DETAIL_RANDOM
@@ -202,7 +209,6 @@ def edit_domain(allow):
# value: 'superuser!' //new value
vals = request.form.to_dict()
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
# domain_name = request.args.get('domain')
answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower()
ub.session.commit()
return ""
@@ -247,6 +253,227 @@ def list_domain(allow):
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@admi.route("/ajax/editrestriction/<int:res_type>", methods=['POST'])
@login_required
@admin_required
def edit_restriction(res_type):
element = request.form.to_dict()
if element['id'].startswith('a'):
if res_type == 0: # Tags as template
elementlist = config.list_allowed_tags()
elementlist[int(element['id'][1:])]=element['Element']
config.config_allowed_tags = ','.join(elementlist)
config.save()
if res_type == 1: # CustomC
elementlist = config.list_allowed_column_values()
elementlist[int(element['id'][1:])]=element['Element']
config.config_allowed_column_value = ','.join(elementlist)
config.save()
if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else:
usr = current_user
elementlist = usr.list_allowed_tags()
elementlist[int(element['id'][1:])]=element['Element']
usr.allowed_tags = ','.join(elementlist)
ub.session.commit()
if res_type == 3: # CColumn per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else:
usr = current_user
elementlist = usr.list_allowed_column_values()
elementlist[int(element['id'][1:])]=element['Element']
usr.allowed_column_value = ','.join(elementlist)
ub.session.commit()
if element['id'].startswith('d'):
if res_type == 0: # Tags as template
elementlist = config.list_denied_tags()
elementlist[int(element['id'][1:])]=element['Element']
config.config_denied_tags = ','.join(elementlist)
config.save()
if res_type == 1: # CustomC
elementlist = config.list_denied_column_values()
elementlist[int(element['id'][1:])]=element['Element']
config.config_denied_column_value = ','.join(elementlist)
config.save()
if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else:
usr = current_user
elementlist = usr.list_denied_tags()
elementlist[int(element['id'][1:])]=element['Element']
usr.denied_tags = ','.join(elementlist)
ub.session.commit()
if res_type == 3: # CColumn per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else:
usr = current_user
elementlist = usr.list_denied_column_values()
elementlist[int(element['id'][1:])]=element['Element']
usr.denied_column_value = ','.join(elementlist)
ub.session.commit()
return ""
def restriction_addition(element, list_func):
elementlist = list_func()
if elementlist == ['']:
elementlist = []
if not element['add_element'] in elementlist:
elementlist += [element['add_element']]
return ','.join(elementlist)
def restriction_deletion(element, list_func):
elementlist = list_func()
if element['Element'] in elementlist:
elementlist.remove(element['Element'])
return ','.join(elementlist)
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
@login_required
@admin_required
def add_restriction(res_type):
element = request.form.to_dict()
if res_type == 0: # Tags as template
if 'submit_allow' in element:
config.config_allowed_tags = restriction_addition(element, config.list_allowed_tags)
config.save()
elif 'submit_deny' in element:
config.config_denied_tags = restriction_addition(element, config.list_denied_tags)
config.save()
if res_type == 1: # CCustom as template
if 'submit_allow' in element:
config.config_allowed_column_value = restriction_addition(element, config.list_denied_column_values)
config.save()
elif 'submit_deny' in element:
config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values)
config.save()
if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else:
usr = current_user
if 'submit_allow' in element:
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
ub.session.commit()
elif 'submit_deny' in element:
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
ub.session.commit()
if res_type == 3: # CustomC per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else:
usr = current_user
if 'submit_allow' in element:
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
ub.session.commit()
elif 'submit_deny' in element:
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
ub.session.commit()
return ""
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
@login_required
@admin_required
def delete_restriction(res_type):
element = request.form.to_dict()
if res_type == 0: # Tags as template
if element['id'].startswith('a'):
config.config_allowed_tags = restriction_deletion(element, config.list_allowed_tags)
config.save()
elif element['id'].startswith('d'):
config.config_denied_tags = restriction_deletion(element, config.list_denied_tags)
config.save()
elif res_type == 1: # CustomC as template
if element['id'].startswith('a'):
config.config_allowed_column_value = restriction_deletion(element, config.list_allowed_column_values)
config.save()
elif element['id'].startswith('d'):
config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values)
config.save()
elif res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else:
usr = current_user
if element['id'].startswith('a'):
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
ub.session.commit()
elif element['id'].startswith('d'):
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
ub.session.commit()
elif res_type == 3: # Columns per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True: # select current user if admins are editing their own rights
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else:
usr = current_user
if element['id'].startswith('a'):
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
ub.session.commit()
elif element['id'].startswith('d'):
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
ub.session.commit()
return ""
#@admi.route("/ajax/listrestriction/<int:type>/<int:user_id>", defaults={'user_id': '0'})
@admi.route("/ajax/listrestriction/<int:res_type>")
@login_required
@admin_required
def list_restriction(res_type):
if res_type == 0: # Tags as template
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
for i,x in enumerate(config.list_denied_tags()) if x != '' ]
allow = [{'Element': x, 'type':_('Allow'), 'id': 'a'+str(i) }
for i,x in enumerate(config.list_allowed_tags()) if x != '']
json_dumps = restrict + allow
elif res_type == 1: # CustomC as template
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
for i,x in enumerate(config.list_denied_column_values()) if x != '' ]
allow = [{'Element': x, 'type':_('Allow'), 'id': 'a'+str(i) }
for i,x in enumerate(config.list_allowed_column_values()) if x != '']
json_dumps = restrict + allow
elif res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == usr_id).first()
else:
usr = current_user
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
for i,x in enumerate(usr.list_denied_tags()) if x != '' ]
allow = [{'Element': x, 'type':_('Allow'), 'id': 'a'+str(i) }
for i,x in enumerate(usr.list_allowed_tags()) if x != '']
json_dumps = restrict + allow
elif res_type == 3: # CustomC per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id==usr_id).first()
else:
usr = current_user
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
for i,x in enumerate(usr.list_denied_column_values()) if x != '' ]
allow = [{'Element': x, 'type':_('Allow'), 'id': 'a'+str(i) }
for i,x in enumerate(usr.list_allowed_column_values()) if x != '']
json_dumps = restrict + allow
else:
json_dumps=""
js = json.dumps(json_dumps)
response = make_response(js.replace("'", '"'))
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@admi.route("/config", methods=["GET", "POST"])
@unconfigured
@@ -262,7 +489,6 @@ def _configuration_update_helper():
db_change = False
to_save = request.form.to_dict()
# _config_dict = lambda x: config.set_from_dictionary(to_save, x, lambda y: y['id'])
_config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
_config_int = lambda x: config.set_from_dictionary(to_save, x, int)
_config_checkbox = lambda x: config.set_from_dictionary(to_save, x, lambda y: y == "on", False)
@@ -280,7 +506,7 @@ def _configuration_update_helper():
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
gdrive_secrets = json.load(settings)['web']
if not gdrive_secrets:
return _configuration_result('client_secrets.json is not configured for web application')
return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
gdriveutils.update_settings(
gdrive_secrets['client_id'],
gdrive_secrets['client_secret'],
@@ -296,50 +522,87 @@ def _configuration_update_helper():
reboot_required |= _config_string("config_keyfile")
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
return _configuration_result('Keyfile location is not valid, please enter correct path', gdriveError)
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), gdriveError)
reboot_required |= _config_string("config_certfile")
if config.config_certfile and not os.path.isfile(config.config_certfile):
return _configuration_result('Certfile location is not valid, please enter correct path', gdriveError)
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), gdriveError)
_config_checkbox_int("config_uploading")
_config_checkbox_int("config_anonbrowse")
_config_checkbox_int("config_public_reg")
reboot_required |= _config_checkbox_int("config_kobo_sync")
_config_checkbox_int("config_kobo_proxy")
_config_int("config_ebookconverter")
_config_string("config_calibre")
_config_string("config_converterpath")
if _config_int("config_login_type"):
reboot_required |= config.config_login_type != constants.LOGIN_STANDARD
reboot_required |= _config_int("config_login_type")
#LDAP configurator,
if config.config_login_type == constants.LOGIN_LDAP:
_config_string("config_ldap_provider_url")
_config_int("config_ldap_port")
_config_string("config_ldap_schema")
_config_string("config_ldap_dn")
_config_string("config_ldap_user_object")
if not config.config_ldap_provider_url or not config.config_ldap_port or not config.config_ldap_dn or not config.config_ldap_user_object:
return _configuration_result('Please enter a LDAP provider, port, DN and user object identifier', gdriveError)
reboot_required |= _config_string("config_ldap_provider_url")
reboot_required |= _config_int("config_ldap_port")
reboot_required |= _config_int("config_ldap_authentication")
reboot_required |= _config_string("config_ldap_dn")
reboot_required |= _config_string("config_ldap_serv_username")
reboot_required |= _config_string("config_ldap_user_object")
reboot_required |= _config_string("config_ldap_group_object_filter")
reboot_required |= _config_string("config_ldap_group_members_field")
reboot_required |= _config_checkbox("config_ldap_openldap")
reboot_required |= _config_int("config_ldap_encryption")
reboot_required |= _config_string("config_ldap_cert_path")
_config_string("config_ldap_group_name")
if "config_ldap_serv_password" in to_save and to_save["config_ldap_serv_password"] != "":
reboot_required |= 1
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8')
config.save()
_config_string("config_ldap_serv_username")
if not config.config_ldap_serv_username or "config_ldap_serv_password" not in to_save:
return _configuration_result('Please enter a LDAP service account and password', gdriveError)
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode)
if not config.config_ldap_provider_url \
or not config.config_ldap_port \
or not config.config_ldap_dn \
or not config.config_ldap_user_object:
return _configuration_result(_('Please Enter a LDAP Provider, '
'Port, DN and User Object Identifier'), gdriveError)
_config_checkbox("config_ldap_use_ssl")
_config_checkbox("config_ldap_use_tls")
_config_checkbox("config_ldap_openldap")
_config_checkbox("config_ldap_require_cert")
_config_string("config_ldap_cert_path")
if config.config_ldap_cert_path and not os.path.isfile(config.config_ldap_cert_path):
return _configuration_result('LDAP Certfile location is not valid, please enter correct path', gdriveError)
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password):
return _configuration_result('Please Enter a LDAP Service Account and Password', gdriveError)
else:
if not config.config_ldap_serv_username:
return _configuration_result('Please Enter a LDAP Service Account', gdriveError)
#_config_checkbox("config_ldap_use_ssl")
#_config_checkbox("config_ldap_use_tls")
# reboot_required |= _config_checkbox("config_ldap_openldap")
# _config_checkbox("config_ldap_require_cert")
if config.config_ldap_group_object_filter:
if config.config_ldap_group_object_filter.count("%s") != 1:
return _configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'),
gdriveError)
if config.config_ldap_group_object_filter.count("(") != config.config_ldap_group_object_filter.count(")"):
return _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'),
gdriveError)
if config.config_ldap_user_object.count("%s") != 1:
return _configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'),
gdriveError)
if config.config_ldap_user_object.count("(") != config.config_ldap_user_object.count(")"):
return _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'),
gdriveError)
if config.config_ldap_cert_path and not os.path.isdir(config.config_ldap_cert_path):
return _configuration_result(_('LDAP Certificate Location is not Valid, Please Enter Correct Path'),
gdriveError)
# Remote login configuration
_config_checkbox("config_remote_login")
if not config.config_remote_login:
ub.session.query(ub.RemoteAuthToken).delete()
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type==0).delete()
# Goodreads configuration
_config_checkbox("config_use_goodreads")
@@ -361,33 +624,32 @@ def _configuration_update_helper():
active_oauths = 0
for element in oauthblueprints:
if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
reboot_required = True
element['oauth_client_id'] = to_save["config_" + str(element['id']) + "_oauth_client_id"]
element['oauth_client_secret'] = to_save["config_" + str(element['id']) + "_oauth_client_secret"]
if to_save["config_"+str(element['id'])+"_oauth_client_id"] \
and to_save["config_"+str(element['id'])+"_oauth_client_secret"]:
active_oauths += 1
element["active"] = 1
ub.session.query(ub.OAuthProvider).filter(ub.OAuthProvider.id == element['id']).update(
{"oauth_client_id":to_save["config_"+str(element['id'])+"_oauth_client_id"],
"oauth_client_secret":to_save["config_"+str(element['id'])+"_oauth_client_secret"],
"active":1})
if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
reboot_required = True
element['oauth_client_id'] = to_save["config_"+str(element['id'])+"_oauth_client_id"]
element['oauth_client_secret'] = to_save["config_"+str(element['id'])+"_oauth_client_secret"]
else:
ub.session.query(ub.OAuthProvider).filter(ub.OAuthProvider.id == element['id']).update(
{"active":0})
element["active"] = 0
ub.session.query(ub.OAuthProvider).filter(ub.OAuthProvider.id == element['id']).update(
{"oauth_client_id":to_save["config_"+str(element['id'])+"_oauth_client_id"],
"oauth_client_secret":to_save["config_"+str(element['id'])+"_oauth_client_secret"],
"active":element["active"]})
_config_int("config_log_level")
_config_string("config_logfile")
reboot_required |= _config_int("config_log_level")
reboot_required |= _config_string("config_logfile")
if not logger.is_valid_logfile(config.config_logfile):
return _configuration_result('Logfile location is not valid, please enter correct path', gdriveError)
return _configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'), gdriveError)
reboot_required |= _config_checkbox_int("config_access_log")
reboot_required |= _config_string("config_access_logfile")
if not logger.is_valid_logfile(config.config_access_logfile):
return _configuration_result('Access Logfile location is not valid, please enter correct path', gdriveError)
return _configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'), gdriveError)
# Rarfile Content configuration
_config_string("config_rarfile_location")
@@ -406,7 +668,7 @@ def _configuration_update_helper():
if db_change:
# reload(db)
if not db.setup_db(config):
return _configuration_result('DB location is not valid, please enter correct path', gdriveError)
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdriveError)
config.save()
flash(_(u"Calibre-Web configuration updated"), category="success")
@@ -432,7 +694,7 @@ def _configuration_result(error_flash=None, gdriveError=None):
show_login_button = config.db_configured and not current_user.is_authenticated
if error_flash:
config.load()
flash(_(error_flash), category="error")
flash(error_flash, category="error")
show_login_button = False
return render_title_template("config_edit.html", config=config, provider=oauthblueprints,
@@ -449,10 +711,11 @@ def new_user():
content = ub.User()
languages = speaking_language()
translations = [LC('en')] + babel.list_translations()
kobo_support = feature_support['kobo'] and config.config_kobo_sync
if request.method == "POST":
to_save = request.form.to_dict()
content.default_language = to_save["default_language"]
content.mature_content = "Show_mature_content" in to_save
# content.mature_content = "Show_mature_content" in to_save
content.locale = to_save.get("locale", content.locale)
content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_'))
@@ -464,7 +727,8 @@ def new_user():
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
flash(_(u"Please fill out all fields!"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
registered_oauth=oauth_check, title=_(u"Add new user"))
registered_oauth=oauth_check, kobo_support=kobo_support,
title=_(u"Add new user"))
content.password = generate_password_hash(to_save["password"])
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\
.first()
@@ -475,15 +739,20 @@ def new_user():
if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
registered_oauth=oauth_check, title=_(u"Add new user"))
registered_oauth=oauth_check, kobo_support=kobo_support,
title=_(u"Add new user"))
else:
content.email = to_save["email"]
else:
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser",
registered_oauth=oauth_check)
kobo_support=kobo_support, registered_oauth=oauth_check)
try:
content.allowed_tags = config.config_allowed_tags
content.denied_tags = config.config_denied_tags
content.allowed_column_value = config.config_allowed_column_value
content.denied_column_value = config.config_denied_column_value
ub.session.add(content)
ub.session.commit()
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
@@ -494,10 +763,9 @@ def new_user():
else:
content.role = config.config_default_role
content.sidebar_view = config.config_default_show
content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT)
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser",
registered_oauth=oauth_check)
kobo_support=kobo_support, registered_oauth=oauth_check)
@admi.route("/admin/mailsettings")
@@ -549,9 +817,13 @@ def update_mailsettings():
@admin_required
def edit_user(user_id):
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
if not content:
flash(_(u"User not found"), category="error")
return redirect(url_for('admin.admin'))
downloads = list()
languages = speaking_language()
translations = babel.list_translations() + [LC('en')]
kobo_support = feature_support['kobo'] and config.config_kobo_sync
for book in content.downloads:
downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
if downloadbook:
@@ -563,9 +835,8 @@ def edit_user(user_id):
if request.method == "POST":
to_save = request.form.to_dict()
if "delete" in to_save:
if ub.session.query(ub.User).filter(and_(ub.User.role.op('&')
(constants.ROLE_ADMIN)== constants.ROLE_ADMIN,
ub.User.id != content.id)).count():
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != content.id).count():
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session.commit()
flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
@@ -574,6 +845,12 @@ def edit_user(user_id):
flash(_(u"No admin user remaining, can't delete user", nick=content.nickname), category="error")
return redirect(url_for('admin.admin'))
else:
if not ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != content.id).count() and \
not 'admin_role' in to_save:
flash(_(u"No admin user remaining, can't remove admin role", nick=content.nickname), category="error")
return redirect(url_for('admin.admin'))
if "password" in to_save and to_save["password"]:
content.password = generate_password_hash(to_save["password"])
anonymous = content.is_anonymous
@@ -597,8 +874,6 @@ def edit_user(user_id):
else:
content.sidebar_view &= ~constants.DETAIL_RANDOM
content.mature_content = "Show_mature_content" in to_save
if "default_language" in to_save:
content.default_language = to_save["default_language"]
if "locale" in to_save and to_save["locale"]:
@@ -610,9 +885,15 @@ def edit_user(user_id):
content.email = to_save["email"]
else:
flash(_(u"Found an existing account for this e-mail address."), category="error")
return render_title_template("user_edit.html", translations=translations, languages=languages,
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
mail_configured = config.get_mail_server_configured(),
new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check,
kobo_support=kobo_support,
new_user=0,
content=content,
downloads=downloads,
registered_oauth=oauth_check,
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
if "nickname" in to_save and to_save["nickname"] != content.nickname:
# Query User nickname, if not existing, change
@@ -627,6 +908,7 @@ def edit_user(user_id):
new_user=0, content=content,
downloads=downloads,
registered_oauth=oauth_check,
kobo_support=kobo_support,
title=_(u"Edit User %(nick)s", nick=content.nickname),
page="edituser")
@@ -638,9 +920,15 @@ def edit_user(user_id):
except IntegrityError:
ub.session.rollback()
flash(_(u"An unknown error occured."), category="error")
return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0,
content=content, downloads=downloads, registered_oauth=oauth_check,
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
new_user=0,
content=content,
downloads=downloads,
registered_oauth=oauth_check,
mail_configured=config.get_mail_server_configured(),
kobo_support=kobo_support,
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
@@ -648,8 +936,6 @@ def edit_user(user_id):
@login_required
@admin_required
def reset_user_password(user_id):
if not config.config_public_reg:
abort(404)
if current_user is not None and current_user.is_authenticated:
ret, message = reset_password(user_id)
if ret == 1:
@@ -671,8 +957,12 @@ def view_logfile():
logfiles = {}
logfiles[0] = logger.get_logfile(config.config_logfile)
logfiles[1] = logger.get_accesslogfile(config.config_access_logfile)
return render_title_template("logviewer.html",title=_(u"Logfile viewer"), accesslog_enable=config.config_access_log,
logfiles=logfiles, page="logfile")
return render_title_template("logviewer.html",
title=_(u"Logfile viewer"),
accesslog_enable=config.config_access_log,
log_enable=bool(config.config_logfile != logger.LOG_TO_STDOUT),
logfiles=logfiles,
page="logfile")
@admi.route("/ajax/log/<int:logtype>")
@@ -717,11 +1007,12 @@ def get_updater_status():
"8": _(u'Update failed:') + u' ' + _(u'HTTP Error'),
"9": _(u'Update failed:') + u' ' + _(u'Connection error'),
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
"11": _(u'Update failed:') + u' ' + _(u'General error')
"11": _(u'Update failed:') + u' ' + _(u'General error'),
"12": _(u'Update failed:') + u' ' + _(u'Update File Could Not be Saved in Temp Dir')
}
status['text'] = text
updater_thread.status = 0
updater_thread.start()
updater_thread.resume()
status['status'] = updater_thread.get_update_status()
elif request.method == "GET":
try:
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2016-2019 jkrehm andy29485 OzzieIsaacs
#
+2 -2
View File
@@ -34,7 +34,7 @@ def version_info():
parser = argparse.ArgumentParser(description='Calibre Web is a web app'
' providing a interface for browsing, reading and downloading eBooks\n', prog='cps.py')
' providing a interface for browsing, reading and downloading eBooks\n', prog='cps.py')
parser.add_argument('-p', metavar='path', help='path and name to settings db, e.g. /opt/cw.db')
parser.add_argument('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db')
parser.add_argument('-c', metavar='path',
@@ -43,7 +43,7 @@ parser.add_argument('-k', metavar='path',
help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile')
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-web',
version=version_info())
parser.add_argument('-i', metavar='ip-adress', help='Server IP-Adress to listen')
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')
args = parser.parse_args()
Executable → Regular
+37 -19
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
@@ -34,13 +33,19 @@ except ImportError as e:
log.debug('cannot import comicapi, extracting comic metadata will not work: %s', e)
import zipfile
import tarfile
try:
import rarfile
use_rarfile = True
except ImportError as e:
log.debug('cannot import rarfile, extracting cover files from rar files will not work: %s', e)
use_rarfile = False
use_comic_meta = False
def extractCover(tmp_file_name, original_file_extension):
def _extractCover(tmp_file_name, original_file_extension, rarExceutable):
cover_data = extension = None
if use_comic_meta:
archive = ComicArchive(tmp_file_name)
cover_data = None
for index, name in enumerate(archive.getPageNameList()):
ext = os.path.splitext(name)
if len(ext) > 1:
@@ -67,6 +72,20 @@ def extractCover(tmp_file_name, original_file_extension):
if extension == '.jpg' or extension == '.jpeg':
cover_data = cf.extractfile(name).read()
break
elif original_file_extension.upper() == '.CBR' and use_rarfile:
try:
rarfile.UNRAR_TOOL = rarExceutable
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 == '.jpg' or extension == '.jpeg':
cover_data = cf.read(name)
break
except Exception as e:
log.debug('Rarfile failed with error: %s', e)
prefix = os.path.dirname(tmp_file_name)
if cover_data:
tmp_cover_name = prefix + '/cover' + extension
@@ -78,9 +97,9 @@ def extractCover(tmp_file_name, original_file_extension):
return tmp_cover_name
def get_comic_info(tmp_file_path, original_file_name, original_file_extension):
def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rarExceutable):
if use_comic_meta:
archive = ComicArchive(tmp_file_path)
archive = ComicArchive(tmp_file_path, rar_exe_path=rarExceutable)
if archive.seemsToBeAComicArchive():
if archive.hasMetadata(MetaDataStyle.CIX):
style = MetaDataStyle.CIX
@@ -101,27 +120,26 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension):
else:
loadedMetadata.language = ""
return BookMeta(
return BookMeta(
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',
cover=extractCover(tmp_file_path, original_file_extension),
cover=_extractCover(tmp_file_path, original_file_extension, rarExceutable),
description=loadedMetadata.comments or "",
tags="",
series=loadedMetadata.series or "",
series_id=loadedMetadata.issue or "",
languages=loadedMetadata.language)
else:
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=original_file_name,
author=u'Unknown',
cover=extractCover(tmp_file_path, original_file_extension),
description="",
tags="",
series="",
series_id="",
languages="")
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=original_file_name,
author=u'Unknown',
cover=_extractCover(tmp_file_path, original_file_extension, rarExceutable),
description="",
tags="",
series="",
series_id="",
languages="")
+72 -25
View File
@@ -22,15 +22,24 @@ import os
import json
import sys
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB
from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger
from . import constants, cli, logger, ub
log = logger.create()
_Base = declarative_base()
class _Flask_Settings(_Base):
__tablename__ = 'flask_settings'
id = Column(Integer, primary_key=True)
flask_session_key = Column(BLOB, default="")
def __init__(self, key):
self.flask_session_key = key
# Baseclass for representing settings in app.db with email server settings and Calibre database settings
# (application settings)
@@ -68,12 +77,18 @@ class _Settings(_Base):
config_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0)
config_remote_login = Column(Boolean, default=False)
config_kobo_sync = Column(Boolean, default=False)
config_default_role = Column(SmallInteger, default=0)
config_default_show = Column(SmallInteger, default=6143)
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
config_columns_to_ignore = Column(String)
config_denied_tags = Column(String, default="")
config_allowed_tags = Column(String, default="")
config_restricted_column = Column(SmallInteger, default=0)
config_denied_column_value = Column(String, default="")
config_allowed_column_value = Column(String, default="")
config_use_google_drive = Column(Boolean, default=False)
config_google_drive_folder = Column(String)
config_google_drive_watch_changes_response = Column(String)
@@ -84,20 +99,22 @@ class _Settings(_Base):
config_login_type = Column(Integer, default=0)
# config_oauth_provider = Column(Integer)
config_kobo_proxy = Column(Boolean, default=False)
config_ldap_provider_url = Column(String, default='localhost')
config_ldap_provider_url = Column(String, default='example.org')
config_ldap_port = Column(SmallInteger, default=389)
config_ldap_schema = Column(String, default='ldap')
config_ldap_serv_username = Column(String)
config_ldap_serv_password = Column(String)
config_ldap_use_ssl = Column(Boolean, default=False)
config_ldap_use_tls = Column(Boolean, default=False)
config_ldap_require_cert = Column(Boolean, default=False)
config_ldap_cert_path = Column(String)
config_ldap_dn = Column(String)
config_ldap_user_object = Column(String)
config_ldap_openldap = Column(Boolean, default=False)
config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE)
config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
config_ldap_serv_password = Column(String, default="")
config_ldap_encryption = Column(SmallInteger, default=0)
config_ldap_cert_path = Column(String, default="")
config_ldap_dn = Column(String, default='dc=example,dc=org')
config_ldap_user_object = Column(String, default='uid=%s')
config_ldap_openldap = Column(Boolean, default=True)
config_ldap_group_object_filter = Column(String, default='(&(objectclass=posixGroup)(cn=%s))')
config_ldap_group_members_field = Column(String, default='memberUid')
config_ldap_group_name = Column(String, default='calibreweb')
config_ebookconverter = Column(Integer, default=0)
config_converterpath = Column(String)
@@ -179,11 +196,20 @@ class _ConfigSQL(object):
def show_detail_random(self):
return self.show_element_new_user(constants.DETAIL_RANDOM)
def show_mature_content(self):
return self.show_element_new_user(constants.MATURE_CONTENT)
def list_denied_tags(self):
mct = self.config_denied_tags.split(",")
return [t.strip() for t in mct]
def mature_content_tags(self):
mct = self.config_mature_content_tags.split(",")
def list_allowed_tags(self):
mct = self.config_allowed_tags.split(",")
return [t.strip() for t in mct]
def list_denied_column_values(self):
mct = self.config_denied_column_value.split(",")
return [t.strip() for t in mct]
def list_allowed_column_values(self):
mct = self.config_allowed_column_value.split(",")
return [t.strip() for t in mct]
def get_log_level(self):
@@ -196,7 +222,7 @@ class _ConfigSQL(object):
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER)
def set_from_dictionary(self, dictionary, field, convertor=None, default=None):
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
'''Possibly updates a field of this object.
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
@@ -212,7 +238,10 @@ class _ConfigSQL(object):
return False
if convertor is not None:
new_value = convertor(new_value)
if encode:
new_value = convertor(new_value.encode(encode))
else:
new_value = convertor(new_value)
current_value = self.__dict__.get(field)
if current_value == new_value:
@@ -261,7 +290,9 @@ class _ConfigSQL(object):
self._session.commit()
self.load()
def invalidate(self):
def invalidate(self, error=None):
if error:
log.error(error)
log.warning("invalidating configuration")
self.db_configured = False
self.config_calibre_dir = None
@@ -279,7 +310,7 @@ def _migrate_table(session, orm_class):
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):
if isinstance(column.default.arg, unicode):
column.default.arg = column.default.arg.encode('utf-8')
if column.default is None:
column_default = ""
@@ -315,6 +346,7 @@ def _migrate_database(session):
# make sure the table is created, if it does not exist
_Base.metadata.create_all(session.bind)
_migrate_table(session, _Settings)
_migrate_table(session, _Flask_Settings)
def load_configuration(session):
@@ -323,5 +355,20 @@ def load_configuration(session):
if not session.query(_Settings).count():
session.add(_Settings())
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()
return conf
return _ConfigSQL(session)
def get_flask_session_key(session):
flask_settings = session.query(_Flask_Settings).one_or_none()
if flask_settings == None:
flask_settings = _Flask_Settings(os.urandom(32))
session.add(flask_settings)
session.commit()
return flask_settings.flask_session_key
+10 -7
View File
@@ -80,9 +80,10 @@ MATURE_CONTENT = 1 << 11
SIDEBAR_PUBLISHER = 1 << 12
SIDEBAR_RATING = 1 << 13
SIDEBAR_FORMAT = 1 << 14
SIDEBAR_ARCHIVED = 1 << 15
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_EDIT_SHELFS & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_FORMAT << 1) - 1
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_ARCHIVED << 1) - 1
UPDATE_STABLE = 0 << 0
AUTO_UPDATE_STABLE = 1 << 0
@@ -92,25 +93,27 @@ AUTO_UPDATE_NIGHTLY = 1 << 2
LOGIN_STANDARD = 0
LOGIN_LDAP = 1
LOGIN_OAUTH = 2
# LOGIN_OAUTH_GOOGLE = 3
LDAP_AUTH_ANONYMOUS = 0
LDAP_AUTH_UNAUTHENTICATE = 1
LDAP_AUTH_SIMPLE = 0
DEFAULT_MAIL_SERVER = "mail.example.org"
DEFAULT_PASSWORD = "admin123"
DEFAULT_PORT = 8083
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
try:
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
DEFAULT_PORT = int(env_CALIBRE_PORT)
except ValueError:
print('Environment variable CALIBRE_PORT has invalid value (%s), faling back to default (8083)' % env_CALIBRE_PORT)
del env_CALIBRE_PORT
EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'}
EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'}
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'}
'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'm4a', 'm4b'}
# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] +
# (['rar','cbr'] if feature_support['rar'] else []))
@@ -126,7 +129,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages')
STABLE_VERSION = {'version': '0.6.5'}
STABLE_VERSION = {'version': '0.6.7'}
NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$'
+14 -10
View File
@@ -25,13 +25,13 @@ import ast
from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy import String, Integer, Boolean
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float, DateTime
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
session = None
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
cc_exceptions = ['datetime', 'comments', 'composite', 'series']
cc_classes = {}
engine = None
@@ -251,10 +251,10 @@ class Books(Base):
title = Column(String)
sort = Column(String)
author_sort = Column(String)
timestamp = Column(String)
timestamp = Column(TIMESTAMP)
pubdate = Column(String)
series_index = Column(String)
last_modified = Column(String)
last_modified = Column(TIMESTAMP)
path = Column(String)
has_cover = Column(Integer)
uuid = Column(String)
@@ -344,14 +344,13 @@ def setup_db(config):
isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False})
conn = engine.connect()
except:
config.invalidate()
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
except Exception as e:
config.invalidate(e)
return False
config.db_configured = True
update_title_sort(config, conn.connection)
# conn.connection.create_function('lower', 1, lcase)
# conn.connection.create_function('upper', 1, ucase)
if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns")
@@ -378,6 +377,11 @@ def setup_db(config):
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id')),
'value': Column(Integer)}
elif row.datatype == 'float':
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id')),
'value': Column(Float)}
else:
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
@@ -385,7 +389,7 @@ def setup_db(config):
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'):
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),
@@ -414,7 +418,7 @@ def dispose():
except: pass
if old_session.bind:
try: old_session.bind.dispose()
except: pass
except Exception: pass
for attr in list(Books.__dict__.keys()):
if attr.startswith("custom_column_"):
+206 -109
View File
@@ -22,7 +22,7 @@
from __future__ import division, print_function, unicode_literals
import os
import datetime
from datetime import datetime
import json
from shutil import move, copyfile
from uuid import uuid4
@@ -47,7 +47,7 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
# passing input_elements not as a list may lead to undesired results
if not isinstance(input_elements, list):
raise TypeError(str(input_elements) + " should be passed as a list")
changed = False
input_elements = [x for x in input_elements if x != '']
# we have all input element (authors, series, tags) names now
# 1. search for elements to remove
@@ -88,6 +88,7 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
if len(del_elements) > 0:
for del_element in del_elements:
db_book_object.remove(del_element)
changed = True
if len(del_element.books) == 0:
db_session.delete(del_element)
# if there are elements to add, we add them now!
@@ -114,37 +115,58 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
else: # db_type should be tag or language
new_element = db_object(add_element)
if db_element is None:
changed = True
db_session.add(new_element)
db_book_object.append(new_element)
else:
if db_type == 'custom':
if db_element.value != add_element:
new_element.value = add_element
# new_element = db_element
elif db_type == 'languages':
if db_element.lang_code != add_element:
db_element.lang_code = add_element
# new_element = db_element
elif db_type == 'series':
if db_element.name != add_element:
db_element.name = add_element # = add_element # new_element = db_object(add_element, add_element)
db_element.name = add_element
db_element.sort = add_element
# new_element = db_element
elif db_type == 'author':
if db_element.name != add_element:
db_element.name = add_element
db_element.sort = add_element.replace('|', ',')
# new_element = db_element
elif db_type == 'publisher':
if db_element.name != add_element:
db_element.name = add_element
db_element.sort = None
# new_element = db_element
elif db_element.name != add_element:
db_element.name = add_element
# new_element = db_element
# add element to book
changed = True
db_book_object.append(db_element)
return changed
def modify_identifiers(input_identifiers, db_identifiers, db_session):
"""Modify Identifiers to match input information.
input_identifiers is a list of read-to-persist Identifiers objects.
db_identifiers is a list of already persisted list of Identifiers objects."""
changed = False
input_dict = dict([ (identifier.type.lower(), identifier) for identifier in input_identifiers ])
db_dict = dict([ (identifier.type.lower(), identifier) for identifier in db_identifiers ])
# delete db identifiers not present in input or modify them with input val
for identifier_type, identifier in db_dict.items():
if identifier_type not in input_dict.keys():
db_session.delete(identifier)
changed = True
else:
input_identifier = input_dict[identifier_type]
identifier.type = input_identifier.type
identifier.val = input_identifier.val
# add input identifiers not present in db
for identifier_type, identifier in input_dict.items():
if identifier_type not in db_dict.keys():
db_session.add(identifier)
changed = True
return changed
@editbook.route("/delete/<int:book_id>/", defaults={'book_format': ""})
@@ -154,53 +176,70 @@ def delete_book(book_id, book_format):
if current_user.role_delete_books():
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
if book:
helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not book_format:
# delete book from Shelfs, Downloads, Read list
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
ub.delete_download(book_id)
ub.session.commit()
try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result:
flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
if not book_format:
# delete book from Shelfs, Downloads, Read list
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
ub.delete_download(book_id)
ub.session.commit()
# check if only this book links to:
# author, language, series, tags, custom columns
modify_database_object([u''], book.authors, db.Authors, db.session, 'author')
modify_database_object([u''], book.tags, db.Tags, db.session, 'tags')
modify_database_object([u''], book.series, db.Series, db.session, 'series')
modify_database_object([u''], book.languages, db.Languages, db.session, 'languages')
modify_database_object([u''], book.publishers, db.Publishers, db.session, 'publishers')
# check if only this book links to:
# author, language, series, tags, custom columns
modify_database_object([u''], book.authors, db.Authors, db.session, 'author')
modify_database_object([u''], book.tags, db.Tags, db.session, 'tags')
modify_database_object([u''], book.series, db.Series, db.session, 'series')
modify_database_object([u''], book.languages, db.Languages, db.session, 'languages')
modify_database_object([u''], book.publishers, db.Publishers, db.session, 'publishers')
cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
if not c.is_multiple:
if len(getattr(book, cc_string)) > 0:
if c.datatype == 'bool' or c.datatype == 'integer':
del_cc = getattr(book, cc_string)[0]
getattr(book, cc_string).remove(del_cc)
db.session.delete(del_cc)
elif c.datatype == 'rating':
del_cc = getattr(book, cc_string)[0]
getattr(book, cc_string).remove(del_cc)
if len(del_cc.books) == 0:
cc = db.session.query(db.Custom_Columns).\
filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
if not c.is_multiple:
if len(getattr(book, cc_string)) > 0:
if c.datatype == 'bool' or c.datatype == 'integer' or c.datatype == 'float':
del_cc = getattr(book, cc_string)[0]
getattr(book, cc_string).remove(del_cc)
log.debug('remove ' + str(c.id))
db.session.delete(del_cc)
else:
del_cc = getattr(book, cc_string)[0]
getattr(book, cc_string).remove(del_cc)
db.session.delete(del_cc)
else:
modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
db.session, 'custom')
db.session.query(db.Books).filter(db.Books.id == book_id).delete()
else:
db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format).delete()
db.session.commit()
db.session.commit()
elif c.datatype == 'rating':
del_cc = getattr(book, cc_string)[0]
getattr(book, cc_string).remove(del_cc)
if len(del_cc.books) == 0:
log.debug('remove ' + str(c.id))
db.session.delete(del_cc)
db.session.commit()
else:
del_cc = getattr(book, cc_string)[0]
getattr(book, cc_string).remove(del_cc)
log.debug('remove ' + str(c.id))
db.session.delete(del_cc)
db.session.commit()
else:
modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
db.session, 'custom')
db.session.query(db.Books).filter(db.Books.id == book_id).delete()
else:
db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete()
db.session.commit()
except Exception as e:
log.debug(e)
db.session.rollback()
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
if book_format:
flash(_('Book Format Successfully Deleted'), category="success")
return redirect(url_for('editbook.edit_book', book_id=book_id))
else:
flash(_('Book Successfully Deleted'), category="success")
return redirect(url_for('web.index'))
@@ -241,10 +280,57 @@ def render_edit_book(book_id):
return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
title=_(u"edit metadata"), page="editbook",
conversion_formats=allowed_conversion_formats,
config=config,
source_formats=valid_source_formats)
def edit_book_ratings(to_save, book):
changed = False
if to_save["rating"].strip():
old_rating = False
if len(book.ratings) > 0:
old_rating = book.ratings[0].rating
ratingx2 = int(float(to_save["rating"]) * 2)
if ratingx2 != old_rating:
changed = True
is_rating = db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first()
if is_rating:
book.ratings.append(is_rating)
else:
new_rating = db.Ratings(rating=ratingx2)
book.ratings.append(new_rating)
if old_rating:
book.ratings.remove(book.ratings[0])
else:
if len(book.ratings) > 0:
book.ratings.remove(book.ratings[0])
changed = True
return changed
def edit_book_languages(to_save, book):
input_languages = to_save["languages"].split(',')
unknown_languages = []
input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages)
for l in unknown_languages:
log.error('%s is not a valid language', l)
flash(_(u"%(langname)s is not a valid language", langname=l), category="error")
return modify_database_object(list(input_l), book.languages, db.Languages, db.session, 'languages')
def edit_book_publisher(to_save, book):
changed = False
if to_save["publisher"]:
publisher = to_save["publisher"].rstrip().strip()
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
changed |= modify_database_object([publisher], book.publishers, db.Publishers, db.session, 'publisher')
elif len(book.publishers):
changed |= modify_database_object([], book.publishers, db.Publishers, db.session, 'publisher')
return changed
def edit_cc_data(book_id, book, to_save):
changed = False
cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
@@ -254,7 +340,7 @@ def edit_cc_data(book_id, book, to_save):
else:
cc_db_value = None
if to_save[cc_string].strip():
if c.datatype == 'int' or c.datatype == 'bool':
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float':
if to_save[cc_string] == 'None':
to_save[cc_string] = None
elif c.datatype == 'bool':
@@ -264,14 +350,17 @@ def edit_cc_data(book_id, book, to_save):
if cc_db_value is not None:
if to_save[cc_string] is not None:
setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string])
changed = True
else:
del_cc = getattr(book, cc_string)[0]
getattr(book, cc_string).remove(del_cc)
db.session.delete(del_cc)
changed = True
else:
cc_class = db.cc_classes[c.id]
new_cc = cc_class(value=to_save[cc_string], book=book_id)
db.session.add(new_cc)
changed = True
else:
if c.datatype == 'rating':
@@ -283,6 +372,7 @@ def edit_cc_data(book_id, book, to_save):
getattr(book, cc_string).remove(del_cc)
if len(del_cc.books) == 0:
db.session.delete(del_cc)
changed = True
cc_class = db.cc_classes[c.id]
new_cc = db.session.query(cc_class).filter(
cc_class.value == to_save[cc_string].strip()).first()
@@ -290,6 +380,7 @@ def edit_cc_data(book_id, book, to_save):
if new_cc is None:
new_cc = cc_class(value=to_save[cc_string].strip())
db.session.add(new_cc)
changed = True
db.session.flush()
new_cc = db.session.query(cc_class).filter(
cc_class.value == to_save[cc_string].strip()).first()
@@ -302,12 +393,13 @@ def edit_cc_data(book_id, book, to_save):
getattr(book, cc_string).remove(del_cc)
if not del_cc.books or len(del_cc.books) == 0:
db.session.delete(del_cc)
changed = True
else:
input_tags = to_save[cc_string].split(',')
input_tags = list(map(lambda it: it.strip(), input_tags))
modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session,
changed |= modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session,
'custom')
return cc
return changed
def upload_single_file(request, book, book_id):
# Check and handle Uploaded file
@@ -361,7 +453,7 @@ def upload_single_file(request, book, book_id):
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")
return uploader.process(
saved_filename, *os.path.splitext(requested_file.filename))
saved_filename, *os.path.splitext(requested_file.filename), config.config_rarfile_location)
def upload_cover(request, book):
@@ -369,11 +461,11 @@ def upload_cover(request, book):
requested_file = request.files['btn-upload-cover']
# check for empty request
if requested_file.filename != '':
if helper.save_cover(requested_file, book.path) is True:
ret, message = helper.save_cover(requested_file, book.path)
if ret is True:
return True
else:
# ToDo Message not always coorect
flash(_(u"Cover is not a supported imageformat (jpg/png/webp), can't save"), category="error")
flash(message, category="error")
return False
return None
@@ -382,6 +474,7 @@ def upload_cover(request, book):
@login_required_if_no_ano
@edit_required
def edit_book(book_id):
modif_date = False
# Show form
if request.method != 'POST':
return render_edit_book(book_id)
@@ -399,6 +492,7 @@ def edit_book(book_id):
meta = upload_single_file(request, book, book_id)
if upload_cover(request, book) is True:
book.has_cover = 1
modif_date = True
try:
to_save = request.form.to_dict()
merge_metadata(to_save, meta)
@@ -410,6 +504,7 @@ def edit_book(book_id):
to_save["book_title"] = _(u'Unknown')
book.title = to_save["book_title"].rstrip().strip()
edited_books_id = book.id
modif_date = True
# handle author(s)
input_authors = to_save["author_name"].split('&')
@@ -418,7 +513,7 @@ def edit_book(book_id):
if input_authors == ['']:
input_authors = [_(u'Unknown')] # prevent empty Author
modify_database_object(input_authors, book.authors, db.Authors, db.session, 'author')
modif_date |= modify_database_object(input_authors, book.authors, db.Authors, db.session, 'author')
# Search for each author if author is in database, if not, authorname and sorted authorname is generated new
# everything then is assembled for sorted author field in database
@@ -434,7 +529,7 @@ def edit_book(book_id):
if book.author_sort != sort_authors:
edited_books_id = book.id
book.author_sort = sort_authors
modif_date = True
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
@@ -445,77 +540,63 @@ def edit_book(book_id):
if not error:
if to_save["cover_url"]:
if helper.save_cover_from_url(to_save["cover_url"], book.path) is True:
result, error = helper.save_cover_from_url(to_save["cover_url"], book.path)
if result is True:
book.has_cover = 1
modif_date = True
else:
flash(_(u"Cover is not a jpg file, can't save"), category="error")
flash(error, category="error")
if book.series_index != to_save["series_index"]:
book.series_index = to_save["series_index"]
modif_date = True
# Handle book comments/description
if len(book.comments):
book.comments[0].text = to_save["description"]
if book.comments[0].text != to_save["description"]:
book.comments[0].text = to_save["description"]
modif_date = True
else:
book.comments.append(db.Comments(text=to_save["description"], book=book.id))
if to_save["description"]:
book.comments.append(db.Comments(text=to_save["description"], book=book.id))
modif_date = True
# Handle identifiers
input_identifiers = identifier_list(to_save, book)
modif_date |= modify_identifiers(input_identifiers, book.identifiers, db.session)
# Handle book tags
input_tags = to_save["tags"].split(',')
input_tags = list(map(lambda it: it.strip(), input_tags))
modify_database_object(input_tags, book.tags, db.Tags, db.session, 'tags')
modif_date |= modify_database_object(input_tags, book.tags, db.Tags, db.session, 'tags')
# Handle book series
input_series = [to_save["series"].strip()]
input_series = [x for x in input_series if x != '']
modify_database_object(input_series, book.series, db.Series, db.session, 'series')
modif_date |= modify_database_object(input_series, book.series, db.Series, db.session, 'series')
if to_save["pubdate"]:
try:
book.pubdate = datetime.datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
except ValueError:
book.pubdate = db.Books.DEFAULT_PUBDATE
else:
book.pubdate = db.Books.DEFAULT_PUBDATE
if to_save["publisher"]:
publisher = to_save["publisher"].rstrip().strip()
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
modify_database_object([publisher], book.publishers, db.Publishers, db.session, 'publisher')
elif len(book.publishers):
modify_database_object([], book.publishers, db.Publishers, db.session, 'publisher')
# handle book publisher
modif_date |= edit_book_publisher(to_save, book)
# handle book languages
input_languages = to_save["languages"].split(',')
unknown_languages = []
input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages)
for l in unknown_languages:
log.error('%s is not a valid language', l)
flash(_(u"%(langname)s is not a valid language", langname=l), category="error")
modify_database_object(list(input_l), book.languages, db.Languages, db.session, 'languages')
modif_date |= edit_book_languages(to_save, book)
# handle book ratings
if to_save["rating"].strip():
old_rating = False
if len(book.ratings) > 0:
old_rating = book.ratings[0].rating
ratingx2 = int(float(to_save["rating"]) * 2)
if ratingx2 != old_rating:
is_rating = db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first()
if is_rating:
book.ratings.append(is_rating)
else:
new_rating = db.Ratings(rating=ratingx2)
book.ratings.append(new_rating)
if old_rating:
book.ratings.remove(book.ratings[0])
else:
if len(book.ratings) > 0:
book.ratings.remove(book.ratings[0])
modif_date |= edit_book_ratings(to_save, book)
# handle cc data
edit_cc_data(book_id, book, to_save)
modif_date |= edit_cc_data(book_id, book, to_save)
if modif_date:
book.last_modified = datetime.utcnow()
db.session.commit()
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
@@ -548,6 +629,19 @@ def merge_metadata(to_save, meta):
to_save["description"] = to_save["description"] or Markup(
getattr(meta, 'description', '')).unescape()
def identifier_list(to_save, book):
"""Generate a list of Identifiers from form information"""
id_type_prefix = 'identifier-type-'
id_val_prefix = 'identifier-val-'
result = []
for type_key, type_value in to_save.items():
if not type_key.startswith(id_type_prefix):
continue
val_key = id_val_prefix + type_key[len(id_type_prefix):]
if val_key not in to_save.keys():
continue
result.append( db.Identifiers(to_save[val_key], type_value, book.id) )
return result
@editbook.route("/upload", methods=["GET", "POST"])
@login_required_if_no_ano
@@ -575,7 +669,7 @@ def upload():
# extract metadata from file
try:
meta = uploader.upload(requested_file)
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir",
@@ -608,16 +702,11 @@ def upload():
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
try:
copyfile(meta.file_path, saved_filename)
except OSError:
log.error("Failed to store file %s (Permission denied)", saved_filename)
flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
try:
os.unlink(meta.file_path)
except OSError:
log.error("Failed to delete file %(file)s (Permission denied)", meta.file_path)
flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path),
category="warning")
except OSError as e:
log.error("Failed to move file %s: %s", saved_filename, e)
flash(_(u"Failed to Move File %(file)s: %(error)s", file=saved_filename, error=e), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
if meta.cover is None:
has_cover = 0
@@ -625,7 +714,15 @@ def upload():
os.path.join(filepath, "cover.jpg"))
else:
has_cover = 1
move(meta.cover, os.path.join(filepath, "cover.jpg"))
try:
copyfile(meta.cover, os.path.join(filepath, "cover.jpg"))
os.unlink(meta.cover)
except OSError as e:
log.error("Failed to move cover file %s: %s", os.path.join(filepath, "cover.jpg"), e)
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=os.path.join(filepath, "cover.jpg"),
error=e),
category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# handle authors
is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first()
@@ -664,8 +761,9 @@ def upload():
# combine path and normalize path from windows systems
path = os.path.join(author_dir, title_dir).replace('\\', '/')
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1),
series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language)
# Calibre adds books with utc as timezone
db_book = db.Books(title, "", db_author.sort, datetime.utcnow(), datetime(101, 1, 1),
series_index, datetime.utcnow(), path, has_cover, db_author, [], db_language)
db_book.authors.append(db_author)
if db_series:
db_book.series.append(db_series)
@@ -697,7 +795,6 @@ def upload():
# Reread book. It's important not to filter the result, as it could have language which hide it from
# current users view (tags are not stored/extracted from metadata and could also be limited)
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
# upload book to gdrive if nesseccary and add "(bookid)" to folder name
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
+192 -96
View File
@@ -96,7 +96,7 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
# read settings and append converter task to queue
if kindle_mail:
settings = config.get_mail_settings()
settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail
settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
# text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title)
else:
@@ -108,7 +108,7 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
return None
else:
error_message = _(u"%(format)s not found: %(fn)s",
format=old_book_format, fn=data.name + "." + old_book_format.lower())
format=old_book_format, fn=data.name + "." + old_book_format.lower())
return error_message
@@ -141,36 +141,52 @@ def check_send_to_kindle(entry):
returns all available book formats for sending to Kindle
"""
if len(entry.data):
bookformats=list()
bookformats = list()
if config.config_ebookconverter == 0:
# no converter - only for mobi and pdf formats
for ele in iter(entry.data):
if 'MOBI' in ele.format:
bookformats.append({'format':'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')})
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')})
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')})
'''if 'AZW3' in ele.format:
bookformats.append({'format':'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')})'''
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
else:
formats = list()
for ele in iter(entry.data):
formats.append(ele.format)
if 'MOBI' in formats:
bookformats.append({'format': 'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')})
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')})
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')})
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if config.config_ebookconverter >= 1:
if 'EPUB' in formats and not 'MOBI' in formats:
bookformats.append({'format': 'Mobi','convert':1,
'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Mobi')})
bookformats.append({'format': 'Mobi',
'convert':1,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Epub',
format='Mobi')})
if config.config_ebookconverter == 2:
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')})
bookformats.append({'format': 'Mobi',
'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Azw3',
format='Mobi')})
return bookformats
else:
log.error(u'Cannot find book entry %d', entry.id)
@@ -204,7 +220,6 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
# returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, kindle_mail)
for entry in iter(book.data):
if entry.format.upper() == book_format.upper():
converted_file_name = entry.name + '.' + book_format.lower()
@@ -277,16 +292,34 @@ def delete_book_file(book, calibrepath, book_format=None):
for file in os.listdir(path):
if file.upper().endswith("."+book_format):
os.remove(os.path.join(path, file))
return True, None
else:
if os.path.isdir(path):
if len(next(os.walk(path))[1]):
log.error("Deleting book %s failed, path has subfolders: %s", book.id, book.path)
return False
shutil.rmtree(path, ignore_errors=True)
return True
return False , _("Deleting book %(id)s failed, path has subfolders: %(path)s",
id=book.id,
path=book.path)
try:
for root, __, files in os.walk(path):
for f in files:
os.unlink(os.path.join(root, f))
shutil.rmtree(path)
except (IOError, OSError) as e:
log.error("Deleting book %s failed: %s", book.id, e)
return False, _("Deleting book %(id)s failed: %(message)s", id=book.id, message=e)
authorpath = os.path.join(calibrepath, os.path.split(book.path)[0])
if not os.listdir(authorpath):
try:
shutil.rmtree(authorpath)
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 False
return False, _("Deleting book %(id)s failed, book path not valid: %(path)s",
id=book.id,
path=book.path)
def update_dir_structure_file(book_id, calibrepath, first_author):
@@ -303,8 +336,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")"
if titledir != new_titledir:
new_title_path = os.path.join(os.path.dirname(path), new_titledir)
try:
new_title_path = os.path.join(os.path.dirname(path), new_titledir)
if not os.path.exists(new_title_path):
os.renames(path, new_title_path)
else:
@@ -321,8 +354,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_title_path, error=str(ex))
if authordir != new_authordir:
new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path))
try:
new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path))
os.renames(path, new_author_path)
localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
except OSError as ex:
@@ -369,7 +402,7 @@ def update_dir_structure_gdrive(book_id, first_author):
path = book.path
gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected
else:
error = _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
error = _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
if authordir != new_authordir:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
@@ -379,7 +412,7 @@ def update_dir_structure_gdrive(book_id, first_author):
path = book.path
gd.updateDatabaseOnEdit(gFile['id'], book.path)
else:
error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
# Rename all files from old names to new names
if authordir != new_authordir or titledir != new_titledir:
@@ -395,7 +428,7 @@ def update_dir_structure_gdrive(book_id, first_author):
def delete_book_gdrive(book, book_format):
error= False
error = None
if book_format:
name = ''
for entry in book.data:
@@ -403,38 +436,42 @@ def delete_book_gdrive(book, book_format):
name = entry.name + '.' + book_format
gFile = gd.getFileFromEbooksFolder(book.path, name)
else:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path),book.path.split('/')[1])
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), book.path.split('/')[1])
if gFile:
gd.deleteDatabaseEntry(gFile['id'])
gFile.Trash()
else:
error =_(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
return error
error = _(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
return error is None, error
def reset_password(user_id):
existing_user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
if not existing_user:
return 0, None
password = generate_random_password()
existing_user.password = generate_password_hash(password)
if not config.get_mail_server_configured():
return (2, None)
return 2, None
try:
ub.session.commit()
send_registration_mail(existing_user.email, existing_user.nickname, password, True)
return (1, existing_user.nickname)
return 1, existing_user.nickname
except Exception:
ub.session.rollback()
return (0, None)
return 0, None
def generate_random_password():
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
passlen = 8
return "".join(random.sample(s,passlen ))
return "".join(random.sample(s, passlen))
################################## External interface
def update_dir_stucture(book_id, calibrepath, first_author = None):
def update_dir_stucture(book_id, calibrepath, first_author=None):
if config.config_use_google_drive:
return update_dir_structure_gdrive(book_id, first_author)
else:
@@ -448,37 +485,54 @@ def delete_book(book, calibrepath, book_format):
return delete_book_file(book, calibrepath, book_format)
def get_book_cover(book_id):
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
if book.has_cover:
def get_cover_on_failure(use_generic_cover):
if use_generic_cover:
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
else:
return None
def get_book_cover(book_id):
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
return get_book_cover_internal(book, use_generic_cover_on_failure=True)
def get_book_cover_with_uuid(book_uuid,
use_generic_cover_on_failure=True):
book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first()
return get_book_cover_internal(book, use_generic_cover_on_failure)
def get_book_cover_internal(book,
use_generic_cover_on_failure):
if book and book.has_cover:
if config.config_use_google_drive:
try:
if not gd.is_gdrive_ready():
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
path=gd.get_cover_via_gdrive(book.path)
return get_cover_on_failure(use_generic_cover_on_failure)
path = gd.get_cover_via_gdrive(book.path)
if path:
return redirect(path)
else:
log.error('%s/cover.jpg not found on Google Drive', book.path)
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
return get_cover_on_failure(use_generic_cover_on_failure)
except Exception as e:
log.exception(e)
# traceback.print_exc()
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
return get_cover_on_failure(use_generic_cover_on_failure)
else:
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
return send_from_directory(cover_file_path, "cover.jpg")
else:
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
return get_cover_on_failure(use_generic_cover_on_failure)
else:
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
return get_cover_on_failure(use_generic_cover_on_failure)
# saves book cover from url
def save_cover_from_url(url, book_path):
img = requests.get(url)
img = requests.get(url, timeout=10) # ToDo: Error Handling
return save_cover(img, book_path)
@@ -494,16 +548,13 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
os.makedirs(filepath)
except OSError:
log.error(u"Failed to create path for cover")
return False
return False, _(u"Failed to create path for cover")
try:
img.save(os.path.join(filepath, saved_filename))
except IOError:
log.error(u"Cover-file is not a valid image file")
return False
except OSError:
log.error(u"Failed to store cover-file")
return False
return True
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
# saves book cover to gdrive or locally
@@ -513,10 +564,10 @@ def save_cover(img, book_path):
if use_PIL:
if content_type not in ('image/jpeg', 'image/png', 'image/webp'):
log.error("Only jpg/jpeg/png/webp files are supported as coverfile")
return False
return False, _("Only jpg/jpeg/png/webp files are supported as coverfile")
# convert to jpg because calibre only supports jpg
if content_type in ('image/png', 'image/webp'):
if hasattr(img,'stream'):
if hasattr(img, 'stream'):
imgc = PILImage.open(img.stream)
else:
imgc = PILImage.open(io.BytesIO(img.content))
@@ -525,24 +576,24 @@ def save_cover(img, book_path):
im.save(tmp_bytesio, format='JPEG')
img._content = tmp_bytesio.getvalue()
else:
if content_type not in ('image/jpeg'):
if content_type not in 'image/jpeg':
log.error("Only jpg/jpeg files are supported as coverfile")
return False
return False, _("Only jpg/jpeg files are supported as coverfile")
if config.config_use_google_drive:
tmpDir = gettempdir()
if save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) is True:
ret, message = save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img)
if ret is True:
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'),
os.path.join(tmpDir, "uploaded_cover.jpg"))
log.info("Cover is saved on Google Drive")
return True
return True, None
else:
return False
return False, message
else:
return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img)
def do_download_file(book, book_format, data, headers):
if config.config_use_google_drive:
startTime = time.time()
@@ -564,13 +615,12 @@ def do_download_file(book, book_format, data, headers):
##################################
def check_unrar(unrarLocation):
if not unrarLocation:
return
if not os.path.exists(unrarLocation):
return 'Unrar binary file not found'
return _('Unrar binary file not found')
try:
if sys.version_info < (3, 0):
@@ -583,16 +633,15 @@ def check_unrar(unrarLocation):
log.debug("unrar version %s", version)
except OSError as err:
log.exception(err)
return 'Error excecuting UnRar'
return _('Error excecuting UnRar')
def json_serial(obj):
"""JSON serializer for objects not serializable by default json code"""
if isinstance(obj, (datetime)):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, (timedelta)):
if isinstance(obj, timedelta):
return {
'__type__': 'timedelta',
'days': obj.days,
@@ -600,7 +649,7 @@ def json_serial(obj):
'microseconds': obj.microseconds,
}
# return obj.isoformat()
raise TypeError ("Type %s not serializable" % type(obj))
raise TypeError("Type %s not serializable" % type(obj))
# helper function for displaying the runtime of tasks
@@ -622,7 +671,7 @@ def format_runtime(runtime):
# helper function to apply localize status information in tasklist entries
def render_task_status(tasklist):
renderedtasklist=list()
renderedtasklist = list()
for task in tasklist:
if task['user'] == current_user.nickname or current_user.role_admin():
if task['formStarttime']:
@@ -638,7 +687,7 @@ def render_task_status(tasklist):
task['runtime'] = format_runtime(task['formRuntime'])
# localize the task status
if isinstance( task['stat'], int ):
if isinstance( task['stat'], int):
if task['stat'] == STAT_WAITING:
task['status'] = _(u'Waiting')
elif task['stat'] == STAT_FAIL:
@@ -651,14 +700,14 @@ def render_task_status(tasklist):
task['status'] = _(u'Unknown Status')
# localize the task type
if isinstance( task['taskType'], int ):
if isinstance( task['taskType'], int):
if task['taskType'] == TASK_EMAIL:
task['taskMessage'] = _(u'E-mail: ') + task['taskMess']
elif task['taskType'] == TASK_CONVERT:
elif task['taskType'] == TASK_CONVERT:
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
elif task['taskType'] == TASK_UPLOAD:
elif task['taskType'] == TASK_UPLOAD:
task['taskMessage'] = _(u'Upload: ') + task['taskMess']
elif task['taskType'] == TASK_CONVERT_ANY:
elif task['taskType'] == TASK_CONVERT_ANY:
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
else:
task['taskMessage'] = _(u'Unknown Task: ') + task['taskMess']
@@ -669,25 +718,58 @@ def render_task_status(tasklist):
# Language and content filters for displaying in the UI
def common_filters():
def common_filters(allow_show_archived=False):
if not allow_show_archived:
archived_books = (
ub.session.query(ub.ArchivedBook)
.filter(ub.ArchivedBook.user_id == int(current_user.id))
.filter(ub.ArchivedBook.is_archived == True)
.all()
)
archived_book_ids = [archived_book.book_id for archived_book in archived_books]
archived_filter = db.Books.id.notin_(archived_book_ids)
else:
archived_filter = true()
if current_user.filter_language() != "all":
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
lang_filter = true()
content_rating_filter = false() if current_user.mature_content else \
db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags()))
return and_(lang_filter, ~content_rating_filter)
negtags_list = current_user.list_denied_tags()
postags_list = current_user.list_allowed_tags()
neg_content_tags_filter = false() if negtags_list == [''] else db.Books.tags.any(db.Tags.name.in_(negtags_list))
pos_content_tags_filter = true() if postags_list == [''] else db.Books.tags.any(db.Tags.name.in_(postags_list))
if config.config_restricted_column:
pos_cc_list = current_user.allowed_column_value.split(',')
pos_content_cc_filter = true() if pos_cc_list == [''] else \
getattr(db.Books, 'custom_column_' + str(config.config_restricted_column)).\
any(db.cc_classes[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(db.Books, 'custom_column_' + str(config.config_restricted_column)).\
any(db.cc_classes[config.config_restricted_column].value.in_(neg_cc_list))
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)
def tags_filters():
return ~(false() if current_user.mature_content else \
db.Tags.name.in_(config.mature_content_tags()))
# return db.session.query(db.Tags).filter(~content_rating_filter).order_by(db.Tags.name).all()
negtags_list = current_user.list_denied_tags()
postags_list = current_user.list_allowed_tags()
neg_content_tags_filter = false() if negtags_list == [''] else db.Tags.name.in_(negtags_list)
pos_content_tags_filter = true() if postags_list == [''] else db.Tags.name.in_(postags_list)
return and_(pos_content_tags_filter, ~neg_content_tags_filter)
# return ~(false()) if postags_list == [''] else db.Tags.in_(postags_list)
# Creates for all stored languages a translated speaking name in the array for the UI
def speaking_language(languages=None):
if not languages:
languages = db.session.query(db.Languages).all()
languages = db.session.query(db.Languages).join(db.books_languages_link).join(db.Books)\
.filter(common_filters())\
.group_by(text('books_languages_link.lang_code')).all()
for lang in languages:
try:
cur_l = LC.parse(lang.lang_code)
@@ -696,6 +778,7 @@ def speaking_language(languages=None):
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
return languages
# 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/
@@ -729,27 +812,36 @@ def order_authors(entry):
# Fill indexpage with all requested data from database
def fill_indexpage(page, database, db_filter, order, *join):
return fill_indexpage_with_archived_books(page, database, db_filter, order, False, *join)
def fill_indexpage_with_archived_books(page, database, db_filter, order, allow_show_archived, *join):
if current_user.show_detail_random():
randm = db.session.query(db.Books).filter(common_filters())\
randm = db.session.query(db.Books).filter(common_filters(allow_show_archived))\
.order_by(func.random()).limit(config.config_random_books)
else:
randm = false()
off = int(int(config.config_books_per_page) * (page - 1))
pagination = Pagination(page, config.config_books_per_page,
len(db.session.query(database).filter(db_filter).filter(common_filters()).all()))
entries = db.session.query(database).join(*join, isouter=True).filter(db_filter).filter(common_filters()).\
order_by(*order).offset(off).limit(config.config_books_per_page).all()
len(db.session.query(database).filter(db_filter)
.filter(common_filters(allow_show_archived)).all()))
entries = db.session.query(database).join(*join, isouter=True).filter(db_filter)\
.filter(common_filters(allow_show_archived))\
.order_by(*order).offset(off).limit(config.config_books_per_page).all()
for book in entries:
book = order_authors(book)
return entries, randm, pagination
def get_typeahead(database, query, replace=('',''), tag_filter=true()):
def get_typeahead(database, query, replace=('', ''), tag_filter=true()):
query = query or ''
db.session.connection().connection.connection.create_function("lower", 1, lcase)
entries = db.session.query(database).filter(tag_filter).filter(func.lower(database.name).ilike("%" + query + "%")).all()
entries = db.session.query(database).filter(tag_filter).\
filter(func.lower(database.name).ilike("%" + query + "%")).all()
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
return json_dumps
# read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(term):
db.session.connection().connection.connection.create_function("lower", 1, lcase)
@@ -766,7 +858,8 @@ def get_search_results(term):
db.Books.authors.any(and_(*q)),
db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + term + "%")),
func.lower(db.Books.title).ilike("%" + term + "%")
)).all()
)).order_by(db.Books.sort).all()
def get_cc_columns():
tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
@@ -774,21 +867,22 @@ def get_cc_columns():
cc = []
for col in tmpcc:
r = re.compile(config.config_columns_to_ignore)
if r.match(col.label):
if not r.match(col.name):
cc.append(col)
else:
cc = tmpcc
return cc
def get_download_link(book_id, book_format):
book_format = book_format.split(".")[0]
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
if book:
data = db.session.query(db.Data).filter(db.Data.book == book.id)\
data1 = db.session.query(db.Data).filter(db.Data.book == book.id)\
.filter(db.Data.format == book_format.upper()).first()
else:
abort(404)
if data:
if data1:
# collect downloaded books only for registered user and not for anonymous user
if current_user.is_authenticated:
ub.update_download(book_id, int(current_user.id))
@@ -798,13 +892,14 @@ def get_download_link(book_id, book_format):
file_name = get_valid_filename(file_name)
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf-8')),
book_format)
return do_download_file(book, book_format, data, headers)
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
quote(file_name.encode('utf-8')), book_format, quote(file_name.encode('utf-8')), book_format)
return do_download_file(book, book_format, data1, headers)
else:
abort(404)
def check_exists_book(authr,title):
def check_exists_book(authr, title):
db.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list()
authorterms = re.split(r'\s*&\s*', authr)
@@ -813,11 +908,12 @@ def check_exists_book(authr,title):
return db.session.query(db.Books).filter(
and_(db.Books.authors.any(and_(*q)),
func.lower(db.Books.title).ilike("%" + title + "%")
)).first()
func.lower(db.Books.title).ilike("%" + title + "%")
)).first()
############### Database Helper functions
def lcase(s):
try:
return unidecode.unidecode(s.lower())
+378 -3
View File
@@ -8,6 +8,8 @@
from __future__ import unicode_literals
# This file is autogenerated, do NOT add, change, or delete ANY string
# If you need help or assistance for adding a new language, please contact the project team
# map iso639 language codes to language names, translated
@@ -852,6 +854,379 @@ LANGUAGE_NAMES = {
"zxx": "Geen linguïstische inhoud",
"zza": "Zaza"
},
"tr": {
"abk": "Abhazca",
"ace": "Achinese",
"ach": "Acoli",
"ada": "Adangme",
"ady": "Adyghe",
"aar": "Afar",
"afh": "Afrihili",
"afr": "Afrikanca",
"ain": "Ainu (Japonca)",
"aka": "Akanca (Afrika dili)",
"akk": "Akatça",
"sqi": "Albanian",
"ale": "Alaskaca",
"amh": "Etiyopyaca",
"anp": "Angika",
"ara": "Arapça",
"arg": "Aragonca (İspanya)",
"arp": "Arapaho (Kuzey Amerika yerlileri)",
"arw": "Arawak (Surinam)",
"hye": "Ermenice",
"asm": "Assamese (Hindistan)",
"ast": "Asturyasca",
"ava": "Avarca",
"ave": "Avestan (Eski İran)",
"awa": "Awadhi (Hindistan)",
"aym": "Aymara (Güney Amerika)",
"aze": "Azerice",
"ban": "Balice (Bali adaları)",
"bal": "Belucice (İran)",
"bam": "Bambara (Mali)",
"bas": "Basa (Kamerun)",
"bak": "Başkırca",
"eus": "Baskça",
"bej": "Beja (Eritre; Sudan)",
"bel": "Beyaz Rusça",
"bem": "Bemba (Zambia)",
"ben": "Bengalce",
"bho": "Bhojpuri (Hindistan)",
"bik": "Bikol (Filipinler)",
"byn": "Bilin",
"bin": "Bini (Afrika)",
"bis": "Bislama (Vanuatu; Kuzey Pasifik)",
"zbl": "Blis Sembolleri",
"bos": "Boşnakça",
"bra": "Braj (Hindistan)",
"bre": "Bretonca",
"bug": "Buginese (Endonezya)",
"bul": "Bulgarca",
"bua": "Buriat (Moğolistan)",
"mya": "Burmaca",
"cad": "Caddo (Kuzey Amerika yerlileri)",
"cat": "Katalanca",
"ceb": "Cebuano (Filipinler)",
"chg": "Çağatayca",
"cha": "Chamorro (Guam adaları)",
"che": "Çeçence",
"chr": "Cherokee (Kuzey Amerika yerlileri)",
"chy": "Cheyenne (kuzey Amerika yerlileri)",
"chb": "Chibcha (Kolombiya)",
"zho": "Çince",
"chn": "Chinook lehçesi (Kuzey Batı Amerika kıyıları)",
"chp": "Chipewyan (Kuzey Amerika yerlileri)",
"cho": "Choctaw (Kuzey Amerika yerlileri)",
"chk": "Chuukese",
"chv": "Çuvaş (Türkçe)",
"cop": "Kıptice (Eski Mısır)",
"cor": "Cornish (Kelt)",
"cos": "Korsikaca",
"cre": "Cree (Kuzey Amerika yerlileri)",
"mus": "Creek",
"hrv": "Hırvatça",
"ces": "Çekçe",
"dak": "Dakota (Kuzey Amerika yerlileri)",
"dan": "Danimarkaca; Danca",
"dar": "Dargwa (Dağıstan)",
"del": "Delaware (Kuzey Amerika yerlileri)",
"div": "Dhivehi",
"din": "Dinka (Sudan)",
"doi": "Dogri (makro dili)",
"dgr": "Dogrib (Kanada)",
"dua": "Duala (Afrika)",
"nld": "Flâmanca (Hollanda dili)",
"dyu": "Dyula (Burkina Faso; Mali)",
"dzo": "Dzongkha (Butan)",
"efi": "Efik (Afrika)",
"egy": "Mısırca (Eski)",
"eka": "Ekajuk (Afrika)",
"elx": "Elamca",
"eng": "İngilizce",
"myv": "Erzya dili",
"epo": "Esperanto",
"est": "Estonca",
"ewe": "Ewe (Afrika)",
"ewo": "Ewondo (Afrika)",
"fan": "Fang (Ekvatoryal Guinea)",
"fat": "Fanti (Afrika)",
"fao": "Faroece",
"fij": "Fiji dili",
"fil": "Filipince",
"fin": "Fince",
"fon": "Fon (Benin)",
"fra": "Fransızca",
"fur": "Friulian (İtalya)",
"ful": "Fulah (Afrika)",
"gaa": "Ganaca",
"glg": "Galce",
"lug": "Ganda Dili",
"gay": "Gayo (Sumatra)",
"gba": "Gbaya (Orta Afrika Cumhuriyeti)",
"gez": "Geez (Etiyopya)",
"kat": "Gürcüce",
"deu": "Almanca",
"gil": "Kiribati dili",
"gon": "Gondi (Hindistan)",
"gor": "Gorontalo (Endonezya)",
"got": "Gotik",
"grb": "Grebo (Liberya)",
"grn": "Guarani (Paraguay)",
"guj": "Gucaratça",
"gwi": "Gwichʼin",
"hai": "Haida (Kuzey Amerika yerlileri)",
"hau": "Hausa Dili",
"haw": "Havai Dili",
"heb": "İbranice",
"her": "Herero Dili",
"hil": "Hiligaynon",
"hin": "Hintçe",
"hmo": "Hiri Motu",
"hit": "Hititçe",
"hmn": "Hmong",
"hun": "Macarca",
"hup": "Hupa",
"iba": "Iban",
"isl": "İzlandaca",
"ido": "Ido Dili",
"ibo": "Igbo Dili",
"ilo": "Iloko",
"ind": "Endonezyaca",
"inh": "İnguşca",
"ina": "Interlingua (Uluslararası Yardımcı Dil Kurumu)",
"ile": "Interlingue",
"iku": "Inuktitut",
"ipk": "Inupiak Dili",
"gle": "İrlandaca",
"ita": "İtalyanca",
"jpn": "Japonca",
"jav": "Cava Dili",
"jrb": "Yahudi-Arapçası",
"jpr": "Yahudi-Farsça",
"kbd": "Kabardian",
"kab": "Kabyle",
"kac": "Kachin",
"kal": "Kalaallisut",
"xal": "Kalmyk",
"kam": "Kamba (Kenya)",
"kan": "Kannada",
"kau": "Kanuri Dili",
"kaa": "Kara-Kalpak",
"krc": "Karachay-Balkar",
"krl": "Karelian",
"kas": "Keşmirce",
"csb": "Kashubian (Lehçe diyalekti)",
"kaw": "Kawi",
"kaz": "Kazakça",
"kha": "Khasi",
"kho": "Khotanese",
"kik": "Kikuyu Dili",
"kmb": "Kimbundu",
"kin": "Kinyarwanda",
"kir": "Kırgızca",
"tlh": "Klingon",
"kom": "Komi Dili",
"kon": "Kongo Dili",
"kok": "Konkani (makro dil)",
"kor": "Korece",
"kos": "Kosraean",
"kpe": "Kpelle",
"kua": "Kuanyama Dili",
"kum": "Kumyk",
"kur": "Kürtçe",
"kru": "Kurukh",
"kut": "Kutenai",
"lad": "Ladino",
"lah": "Lahnda",
"lam": "Lamba",
"lao": "Laos Dili",
"lat": "Latince",
"lav": "Letonca",
"lez": "Lezghian",
"lim": "Liburg Dili",
"lin": "Lingala Dili",
"lit": "Litvanyaca",
"jbo": "Lojban dili",
"loz": "Lozi",
"lub": "Luba Katanga Dili",
"lua": "Luba-Lulua",
"lui": "Luiseno",
"smj": "Lule Sami",
"lun": "Lunda",
"luo": "Luo (Kenya ve Tanzanya)",
"lus": "Lushai",
"ltz": "Lüksemburg Dili",
"mkd": "Makedonca",
"mad": "Madurese",
"mag": "Magahi",
"mai": "Maithili dili",
"mak": "Makasar",
"mlg": "Madagaskar Dili",
"msa": "Malay (makro dili)",
"mal": "Malayalam",
"mlt": "Maltaca",
"mnc": "Manchu",
"mdr": "Mandar",
"man": "Mandingo",
"mni": "Manipuri dili",
"glv": "Manx (Galler)",
"mri": "Maori Dili",
"arn": "Mapudungun",
"mar": "Marathi",
"chm": "Mari (Rusya)",
"mah": "Marshall Dili",
"mwr": "Marwari",
"mas": "Masai",
"men": "Mende (Sierra Leone)",
"mic": "Mi'kmak",
"min": "Minangkabau",
"mwl": "Mirandese",
"moh": "Mohawk",
"mdf": "Moşka",
"lol": "Mongo",
"mon": "Moğol Dili",
"mos": "Mossi",
"mul": "Çoklu diller",
"nqo": "N'Ko",
"nau": "Nauru",
"nav": "Navajo Dili",
"ndo": "Ndonga Dili",
"nap": "Neapolitan",
"nia": "Nias",
"niu": "Niuean",
"zxx": "Hiçbir dil içeriği yok",
"nog": "Nogai",
"nor": "Norveçce",
"nob": "Norveççe Bokmal",
"nno": "Norveççe Nynorsk",
"nym": "Nyamwezi",
"nya": "Nyanja",
"nyn": "Nyankole",
"nyo": "Nyoro",
"nzi": "Nzima",
"oci": "Oksitanca (1500 sonrası)",
"oji": "Ojibwa Dili",
"orm": "Oromo Dili",
"osa": "Osage",
"oss": "Osetya Dili",
"pal": "Pehlevi",
"pau": "Palauan",
"pli": "Pali Dili",
"pam": "Pampanga",
"pag": "Pangasinan",
"pan": "Pencabi Dili",
"pap": "Papiamento",
"fas": "Farsça",
"phn": "Fenikçe",
"pon": "Pohnpeian",
"pol": "Polonyaca",
"por": "Portekizce",
"pus": "Pushto",
"que": "Quechua",
"raj": "Rajasthani",
"rap": "Rapanui",
"ron": "Rumence",
"roh": "Romanca",
"rom": "Çingene Dili",
"run": "Kirundi",
"rus": "Rusça",
"smo": "Samoa Dili",
"sad": "Sandawe",
"sag": "Sangho",
"san": "Sanskritçe",
"sat": "Santali dili",
"srd": "Sardinya",
"sas": "Sasak",
"sco": "İskoç lehçesi",
"sel": "Selkup",
"srp": "Sırpça",
"srr": "Serer",
"shn": "Shan",
"sna": "Shona",
"scn": "Sicilyalı",
"sid": "Sidamo",
"bla": "Siksika (Kuzey Amerika yerlileri)",
"snd": "Sindhi",
"sin": "Sinhala Dili",
"den": "Slave (Athapascan; Kuzey Amerika yerlileri)",
"slk": "Slovakça",
"slv": "Slovence",
"sog": "Sogdian",
"som": "Somali Dili",
"snk": "Soninke",
"spa": "İspanyolca",
"srn": "Sranan Tongo",
"suk": "Sukuma",
"sux": "Sümerce",
"sun": "Sudan Dili",
"sus": "Susu",
"swa": "Swahili (makro dil)",
"ssw": "Siswati",
"swe": "İsveçce",
"syr": "Süryanice",
"tgl": "Tagalog",
"tah": "Tahitice",
"tgk": "Tacikçe",
"tmh": "Tamashek",
"tam": "Tamilce",
"tat": "Tatarca",
"tel": "Telugu",
"ter": "Tereno",
"tet": "Tetum",
"tha": "Taylandça",
"bod": "Tibetçe",
"tig": "Tigre",
"tir": "Tigrinya Dili",
"tem": "Timne",
"tiv": "Tiv",
"tli": "Tlingit",
"tpi": "Tok Pisin",
"tkl": "Tokelau",
"tog": "Tonga (Nyasa)",
"ton": "Tonga",
"tsi": "Tsimshian",
"tso": "Tsonga",
"tsn": "Setswana",
"tum": "Tumbuka",
"tur": "Türkçe",
"tuk": "Türkmence",
"tvl": "Tuvalu",
"tyv": "Tuvinian",
"twi": "Twi",
"udm": "Udmurt",
"uga": "Ugarit Çivi Yazısı",
"uig": "Uygurca",
"ukr": "Ukraynaca",
"umb": "Umbundu",
"mis": "Şifresiz diller",
"und": "Belirlenemeyen",
"urd": "Urduca",
"uzb": "Özbekçe",
"vai": "Vai",
"ven": "Venda Dili",
"vie": "Vietnamca",
"vol": "Volapük",
"vot": "Votic",
"wln": "Valonca",
"war": "Waray (Filipinler)",
"was": "Vasho",
"cym": "Gal Dili",
"wal": "Wolaytta",
"wol": "Wolof",
"xho": "Xhosa",
"sah": "Yakut",
"yao": "Yao",
"yap": "Yapese",
"yid": "Yidiş",
"yor": "Yoruba",
"zap": "Zapotec",
"zza": "Zaza",
"zen": "Zenaga",
"zha": "Zuang Dili",
"zul": "Zulu",
"zun": "Zuni"
},
"fr": {
"aar": "afar",
"abk": "abkhaze",
@@ -4935,7 +5310,7 @@ LANGUAGE_NAMES = {
"pon": "Pohnpeian",
"por": "Portuguese",
"pro": "Provençal; Old (to 1500)",
"pus": "Pushto",
"pus": "Pashto",
"que": "Quechua",
"raj": "Rajasthani",
"rap": "Rapanui",
@@ -6195,7 +6570,7 @@ LANGUAGE_NAMES = {
"pon": "Pohnpeian",
"por": "Portuguese",
"pro": "Provençal; Old (to 1500)",
"pus": "Pushto",
"pus": "Pashto",
"que": "Quechua",
"raj": "Rajasthani",
"rap": "Rapanui",
@@ -6615,7 +6990,7 @@ LANGUAGE_NAMES = {
"pon": "Pohnpeian",
"por": "Portuguese",
"pro": "Provençal; Old (to 1500)",
"pus": "Pushto",
"pus": "Pashto",
"que": "Quechua",
"raj": "Rajasthani",
"rap": "Rapanui",
+5 -2
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
@@ -81,9 +80,13 @@ def formatdate_filter(val):
formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
return format_date(formatdate, format='medium', locale=get_locale())
except AttributeError as e:
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, current_user.locale, current_user.nickname)
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
current_user.locale,
current_user.nickname
)
return formatdate
@jinjia.app_template_filter('formatdateinput')
def format_date_input(val):
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
+1092
View File
File diff suppressed because it is too large Load Diff
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 shavitmichael, 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/>.
"""This module is used to control authentication/authorization of Kobo sync requests.
This module also includes research notes into the auth protocol used by Kobo devices.
Log-in:
When first booting a Kobo device the user must sign into a Kobo (or affiliate) account.
Upon successful sign-in, the user is redirected to
https://auth.kobobooks.com/CrossDomainSignIn?id=<some id>
which serves the following response:
<script type='text/javascript'>
location.href='kobo://UserAuthenticated?userId=<redacted>&userKey<redacted>&email=<redacted>&returnUrl=https%3a%2f%2fwww.kobo.com';
</script>
And triggers the insertion of a userKey into the device's User table.
Together, the device's DeviceId and UserKey act as an *irrevocable* authentication
token to most (if not all) Kobo APIs. In fact, in most cases only the UserKey is
required to authorize the API call.
Changing Kobo password *does not* invalidate user keys! This is apparently a known
issue for a few years now https://www.mobileread.com/forums/showpost.php?p=3476851&postcount=13
(although this poster hypothesised that Kobo could blacklist a DeviceId, many endpoints
will still grant access given the userkey.)
Official Kobo Store Api authorization:
* For most of the endpoints we care about (sync, metadata, tags, etc), the userKey is
passed in the x-kobo-userkey header, and is sufficient to authorize the API call.
* Some endpoints (e.g: AnnotationService) instead make use of Bearer tokens pass through
an authorization header. To get a BearerToken, the device makes a POST request to the
v1/auth/device endpoint with the secret UserKey and the device's DeviceId.
* The book download endpoint passes an auth token as a URL param instead of a header.
Our implementation:
We pretty much ignore all of the above. To authenticate the user, we generate a random
and unique token that they append to the CalibreWeb Url when setting up the api_store
setting on the device.
Thus, every request from the device to the api_store will hit CalibreWeb with the
auth_token in the url (e.g: https://mylibrary.com/<auth_token>/v1/library/sync).
In addition, once authenticated we also set the login cookie on the response that will
be sent back for the duration of the session to authorize subsequent API calls (in
particular calls to non-Kobo specific endpoints such as the CalibreWeb book download).
"""
from binascii import hexlify
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_babel import gettext as _
from . import logger, ub, lm
from .web import render_title_template
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
log = logger.create()
def register_url_value_preprocessor(kobo):
@kobo.url_value_preprocessor
def pop_auth_token(__, values):
g.auth_token = values.pop("auth_token")
def disable_failed_auth_redirect_for_blueprint(bp):
lm.blueprint_login_views[bp.name] = None
def get_auth_token():
if "auth_token" in g:
return g.get("auth_token")
else:
return None
def requires_kobo_auth(f):
@wraps(f)
def inner(*args, **kwargs):
auth_token = get_auth_token()
if auth_token is not None:
user = (
ub.session.query(ub.User)
.join(ub.RemoteAuthToken)
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
.first()
)
if user is not None:
login_user(user)
return f(*args, **kwargs)
log.debug("Received Kobo request without a recognizable auth token.")
return abort(401)
return inner
kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
@kobo_auth.route("/generate_auth_token/<int:user_id>")
@login_required
def generate_auth_token(user_id):
host = ':'.join(request.host.rsplit(':')[0:-1])
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'):
warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device')
return render_title_template(
"generate_kobo_auth_url.html",
title=_(u"Kobo Setup"),
warning = warning
)
else:
# Invalidate any prevously generated Kobo Auth token for this user.
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
ub.RemoteAuthToken.user_id == user_id
).filter(ub.RemoteAuthToken.token_type==1).first()
if not auth_token:
auth_token = ub.RemoteAuthToken()
auth_token.user_id = user_id
auth_token.expiration = datetime.max
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
auth_token.token_type = 1
ub.session.add(auth_token)
ub.session.commit()
return render_title_template(
"generate_kobo_auth_url.html",
title=_(u"Kobo Setup"),
kobo_auth_url=url_for(
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
),
warning = False
)
@kobo_auth.route("/deleteauthtoken/<int:user_id>")
@login_required
def delete_auth_token(user_id):
# Invalidate any prevously generated Kobo Auth token for this user.
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
.filter(ub.RemoteAuthToken.token_type==1).delete()
ub.session.commit()
return ""
+6 -2
View File
@@ -67,6 +67,8 @@ def get_level_name(level):
def is_valid_logfile(file_path):
if file_path == LOG_TO_STDERR or file_path == LOG_TO_STDOUT:
return True
if not file_path:
return True
if os.path.isdir(file_path):
@@ -105,7 +107,9 @@ def setup(log_file, log_level=None):
# avoid spamming the log with debug messages from libraries
r.setLevel(log_level)
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
# Otherwise name get's destroyed on windows
if log_file != LOG_TO_STDERR and log_file != LOG_TO_STDOUT:
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
previous_handler = r.handlers[0] if r.handlers else None
if previous_handler:
@@ -119,7 +123,7 @@ def setup(log_file, log_level=None):
file_handler = StreamHandler(sys.stdout)
file_handler.baseFilename = log_file
else:
file_handler = StreamHandler()
file_handler = StreamHandler(sys.stderr)
file_handler.baseFilename = log_file
else:
try:
+15 -5
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
@@ -24,7 +23,18 @@ from flask import session
try:
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound
backend_resultcode = False # prevent storing values with this resultcode
except ImportError:
# fails on flask-dance >1.3, due to renaming
try:
from flask_dance.consumer.storage.sqla import SQLAlchemyStorage as SQLAlchemyBackend
from flask_dance.consumer.storage.sqla import first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound
backend_resultcode = True # prevent storing values with this resultcode
except ImportError:
pass
try:
class OAuthBackend(SQLAlchemyBackend):
"""
Stores and retrieves OAuth tokens using a relational database through
@@ -40,7 +50,7 @@ try:
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[blueprint.name + '_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)
@@ -62,7 +72,7 @@ try:
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")
# raise ValueError("Cannot get OAuth token without an associated user")
return None
# check for user ID
if hasattr(self.model, "user_id") and uid:
@@ -87,7 +97,7 @@ try:
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")))
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")
@@ -153,5 +163,5 @@ try:
blueprint=blueprint, user=user, user_id=user_id,
))
except ImportError:
except Exception:
pass
+82 -73
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
@@ -36,8 +35,7 @@ from sqlalchemy.orm.exc import NoResultFound
from . import constants, logger, config, app, ub
from .web import login_required
from .oauth import OAuthBackend
# from .web import github_oauth_required
from .oauth import OAuthBackend, backend_resultcode
oauth_check = {}
@@ -50,7 +48,7 @@ def oauth_required(f):
def inner(*args, **kwargs):
if config.config_login_type == constants.LOGIN_OAUTH:
return f(*args, **kwargs)
if request.is_xhr:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
data = {'status': 'error', 'message': 'Not Found'}
response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8"
@@ -60,29 +58,29 @@ def oauth_required(f):
return inner
def register_oauth_blueprint(id, show_name):
oauth_check[id] = show_name
def register_oauth_blueprint(cid, show_name):
oauth_check[cid] = show_name
def register_user_with_oauth(user=None):
all_oauth = {}
for oauth in oauth_check.keys():
if str(oauth) + '_oauth_user_id' in session and session[str(oauth) + '_oauth_user_id'] != '':
all_oauth[oauth] = oauth_check[oauth]
for oauth_key in oauth_check.keys():
if str(oauth_key) + '_oauth_user_id' in session and session[str(oauth_key) + '_oauth_user_id'] != '':
all_oauth[oauth_key] = oauth_check[oauth_key]
if len(all_oauth.keys()) == 0:
return
if user is None:
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
else:
for oauth in all_oauth.keys():
for oauth_key in all_oauth.keys():
# Find this OAuth token in the database, or create it
query = ub.session.query(ub.OAuth).filter_by(
provider=oauth,
provider_user_id=session[str(oauth) + "_oauth_user_id"],
provider=oauth_key,
provider_user_id=session[str(oauth_key) + "_oauth_user_id"],
)
try:
oauth = query.one()
oauth.user_id = user.id
oauth_key = query.one()
oauth_key.user_id = user.id
except NoResultFound:
# no found, return error
return
@@ -94,39 +92,40 @@ def register_user_with_oauth(user=None):
def logout_oauth_user():
for oauth in oauth_check.keys():
if str(oauth) + '_oauth_user_id' in session:
session.pop(str(oauth) + '_oauth_user_id')
for oauth_key in oauth_check.keys():
if str(oauth_key) + '_oauth_user_id' in session:
session.pop(str(oauth_key) + '_oauth_user_id')
if ub.oauth_support:
oauthblueprints =[]
oauthblueprints = []
if not ub.session.query(ub.OAuthProvider).count():
oauth = ub.OAuthProvider()
oauth.provider_name = "github"
oauth.active = False
ub.session.add(oauth)
oauthProvider = ub.OAuthProvider()
oauthProvider.provider_name = "github"
oauthProvider.active = False
ub.session.add(oauthProvider)
ub.session.commit()
oauth = ub.OAuthProvider()
oauth.provider_name = "google"
oauth.active = False
ub.session.add(oauth)
oauthProvider = ub.OAuthProvider()
oauthProvider.provider_name = "google"
oauthProvider.active = False
ub.session.add(oauthProvider)
ub.session.commit()
oauth_ids = ub.session.query(ub.OAuthProvider).all()
ele1=dict(provider_name='github',
id=oauth_ids[0].id,
active=oauth_ids[0].active,
oauth_client_id=oauth_ids[0].oauth_client_id,
scope=None,
oauth_client_secret=oauth_ids[0].oauth_client_secret,
obtain_link='https://github.com/settings/developers')
ele2=dict(provider_name='google',
id=oauth_ids[1].id,
active=oauth_ids[1].active,
scope=["https://www.googleapis.com/auth/plus.me", "https://www.googleapis.com/auth/userinfo.email"],
oauth_client_id=oauth_ids[1].oauth_client_id,
oauth_client_secret=oauth_ids[1].oauth_client_secret,
obtain_link='https://github.com/settings/developers')
ele1 = dict(provider_name='github',
id=oauth_ids[0].id,
active=oauth_ids[0].active,
oauth_client_id=oauth_ids[0].oauth_client_id,
scope=None,
oauth_client_secret=oauth_ids[0].oauth_client_secret,
obtain_link='https://github.com/settings/developers')
ele2 = dict(provider_name='google',
id=oauth_ids[1].id,
active=oauth_ids[1].active,
scope=["https://www.googleapis.com/auth/plus.me", "https://www.googleapis.com/auth/userinfo.email"],
oauth_client_id=oauth_ids[1].oauth_client_id,
oauth_client_secret=oauth_ids[1].oauth_client_secret,
obtain_link='https://github.com/settings/developers')
oauthblueprints.append(ele1)
oauthblueprints.append(ele2)
@@ -139,12 +138,12 @@ if ub.oauth_support:
client_id=element['oauth_client_id'],
client_secret=element['oauth_client_secret'],
redirect_to="oauth."+element['provider_name']+"_login",
scope = element['scope']
scope=element['scope']
)
element['blueprint']=blueprint
app.register_blueprint(blueprint, url_prefix="/login")
element['blueprint'] = blueprint
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
user=current_user, user_required=True)
app.register_blueprint(blueprint, url_prefix="/login")
if element['active']:
register_oauth_blueprint(element['id'], element['provider_name'])
@@ -191,54 +190,64 @@ if ub.oauth_support:
provider_user_id=provider_user_id,
)
try:
oauth = query.one()
oauth_entry = query.one()
# update token
oauth.token = token
oauth_entry.token = token
except NoResultFound:
oauth = ub.OAuth(
oauth_entry = ub.OAuth(
provider=provider_id,
provider_user_id=provider_user_id,
token=token,
)
try:
ub.session.add(oauth)
ub.session.add(oauth_entry)
ub.session.commit()
except Exception as e:
log.exception(e)
ub.session.rollback()
# Disable Flask-Dance's default behavior for saving the OAuth token
return False
# Value differrs depending on flask-dance version
return backend_resultcode
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url):
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 = query.one()
oauth_entry = query.first()
# already bind with user, just login
if oauth.user:
login_user(oauth.user)
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.user = current_user
oauth_entry.user = current_user
try:
ub.session.add(oauth)
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.exception(e)
ub.session.rollback()
return redirect(url_for('web.login'))
#if config.config_public_reg:
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:
# else:
# flash(_(u"Public registration is not enabled"), category="error")
# return redirect(url_for(redirect_url))
except NoResultFound:
except (NoResultFound, AttributeError):
return redirect(url_for(redirect_url))
@@ -249,8 +258,8 @@ if ub.oauth_support:
)
try:
oauths = query.all()
for oauth in oauths:
status.append(int(oauth.provider))
for oauth_entry in oauths:
status.append(int(oauth_entry.provider))
return status
except NoResultFound:
return None
@@ -264,21 +273,21 @@ if ub.oauth_support:
user_id=current_user.id,
)
try:
oauth = query.one()
oauth_entry = query.one()
if current_user and current_user.is_authenticated:
oauth.user = current_user
oauth_entry.user = current_user
try:
ub.session.delete(oauth)
ub.session.delete(oauth_entry)
ub.session.commit()
logout_oauth_user()
flash(_(u"Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success")
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
except Exception as e:
log.exception(e)
ub.session.rollback()
flash(_(u"Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error")
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound:
log.warning("oauth %s for user %d not fount", provider, current_user.id)
flash(_(u"Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error")
log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_(u"Not Linked to %(oauth)s.", oauth=oauth_check[provider]), category="error")
return redirect(url_for('web.profile'))
@@ -293,11 +302,11 @@ if ub.oauth_support:
error=error,
description=error_description,
uri=error_uri,
) # ToDo: Translate
) # ToDo: Translate
flash(msg, category="error")
@oauth.route('/github')
@oauth.route('/link/github')
@oauth_required
def github_login():
if not github.authorized:
@@ -305,7 +314,7 @@ if ub.oauth_support:
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')
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'))
@@ -316,7 +325,7 @@ if ub.oauth_support:
return unlink_oauth(oauthblueprints[0]['id'])
@oauth.route('/login/google')
@oauth.route('/link/google')
@oauth_required
def google_login():
if not google.authorized:
@@ -324,7 +333,7 @@ if ub.oauth_support:
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')
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'))
@@ -339,7 +348,7 @@ if ub.oauth_support:
error=error,
description=error_description,
uri=error_uri,
) # ToDo: Translate
) # ToDo: Translate
flash(msg, category="error")
+92 -41
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
@@ -57,6 +56,20 @@ def requires_basic_auth_if_no_ano(f):
return decorated
class FeedObject:
def __init__(self, rating_id, rating_name):
self.rating_id = rating_id
self.rating_name = rating_name
@property
def id(self):
return self.rating_id
@property
def name(self):
return self.rating_name
@opds.route("/opds/")
@opds.route("/opds")
@requires_basic_auth_if_no_ano
@@ -88,7 +101,7 @@ def feed_normal_search():
def feed_new():
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books, True, [db.Books.timestamp.desc()])
db.Books, True, [db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@@ -106,7 +119,8 @@ def feed_discover():
def feed_best_rated():
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books, db.Books.ratings.any(db.Ratings.rating > 9), [db.Books.timestamp.desc()])
db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@@ -140,7 +154,8 @@ def feed_hot():
def feed_authorindex():
off = request.args.get("offset") or 0
entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\
.group_by(text('books_authors_link.author')).order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off)
.group_by(text('books_authors_link.author')).order_by(db.Authors.sort).limit(config.config_books_per_page)\
.offset(off)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(db.session.query(db.Authors).all()))
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
@@ -151,7 +166,9 @@ def feed_authorindex():
def feed_author(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books, db.Books.authors.any(db.Authors.id == book_id), [db.Books.timestamp.desc()])
db.Books,
db.Books.authors.any(db.Authors.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@@ -160,7 +177,8 @@ def feed_author(book_id):
def feed_publisherindex():
off = request.args.get("offset") or 0
entries = db.session.query(db.Publishers).join(db.books_publishers_link).join(db.Books).filter(common_filters())\
.group_by(text('books_publishers_link.publisher')).order_by(db.Publishers.sort).limit(config.config_books_per_page).offset(off)
.group_by(text('books_publishers_link.publisher')).order_by(db.Publishers.sort)\
.limit(config.config_books_per_page).offset(off)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(db.session.query(db.Publishers).all()))
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination)
@@ -171,7 +189,8 @@ def feed_publisherindex():
def feed_publisher(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books, db.Books.publishers.any(db.Publishers.id == book_id),
db.Books,
db.Books.publishers.any(db.Publishers.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@@ -192,7 +211,9 @@ def feed_categoryindex():
def feed_category(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books, db.Books.tags.any(db.Tags.id == book_id), [db.Books.timestamp.desc()])
db.Books,
db.Books.tags.any(db.Tags.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@@ -212,9 +233,40 @@ def feed_seriesindex():
def feed_series(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books, db.Books.series.any(db.Series.id == book_id), [db.Books.series_index])
db.Books,
db.Books.series.any(db.Series.id == book_id),
[db.Books.series_index])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/ratings")
@requires_basic_auth_if_no_ano
def feed_ratingindex():
off = request.args.get("offset") or 0
entries = db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
(db.Ratings.rating / 2).label('name')) \
.join(db.books_ratings_link).join(db.Books).filter(common_filters()) \
.group_by(text('books_ratings_link.rating')).order_by(db.Ratings.rating).all()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries))
element = list()
for entry in entries:
element.append(FeedObject(entry[0].id, "{} Stars".format(entry.name)))
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
@opds.route("/opds/ratings/<book_id>")
@requires_basic_auth_if_no_ano
def feed_ratings(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books,
db.Books.ratings.any(db.Ratings.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/formats")
@requires_basic_auth_if_no_ano
def feed_formatindex():
@@ -223,10 +275,11 @@ def feed_formatindex():
.group_by(db.Data.format).order_by(db.Data.format).all()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries))
element = list()
for entry in entries:
entry.name = entry.format
entry.id = entry.format
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_format', pagination=pagination)
element.append(FeedObject(entry.format, entry.format))
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination)
@opds.route("/opds/formats/<book_id>")
@@ -234,9 +287,12 @@ def feed_formatindex():
def feed_format(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books, db.Books.data.any(db.Data.format == book_id.upper()), [db.Books.timestamp.desc()])
db.Books,
db.Books.data.any(db.Data.format == book_id.upper()),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/language")
@opds.route("/opds/language/")
@requires_basic_auth_if_no_ano
@@ -265,28 +321,18 @@ def feed_languagesindex():
def feed_languages(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
db.Books, db.Books.languages.any(db.Languages.id == book_id), [db.Books.timestamp.desc()])
'''for entry in entries:
for index in range(0, len(entry.languages)):
try:
entry.languages[index].language_name = LC.parse(entry.languages[index].lang_code).get_language_name(
get_locale())
except UnknownLocaleError:
entry.languages[index].language_name = _(
isoLanguages.get(part3=entry.languages[index].lang_code).name)'''
db.Books,
db.Books.languages.any(db.Languages.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/shelfindex", defaults={'public': 0})
@opds.route("/opds/shelfindex/<string:public>")
@opds.route("/opds/shelfindex")
@requires_basic_auth_if_no_ano
def feed_shelfindex(public):
def feed_shelfindex():
off = request.args.get("offset") or 0
if public != 0:
shelf = g.public_shelfes
number = len(shelf)
else:
shelf = g.user.shelf
number = shelf.count()
shelf = g.shelves_access
number = len(shelf)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
number)
return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination)
@@ -297,7 +343,8 @@ def feed_shelfindex(public):
def feed_shelf(book_id):
off = request.args.get("offset") or 0
if current_user.is_anonymous:
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first()
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1,
ub.Shelf.id == book_id).first()
else:
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
ub.Shelf.id == book_id),
@@ -311,20 +358,20 @@ def feed_shelf(book_id):
for book in books_in_shelf:
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
result.append(cur_book)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(result))
return render_xml_template('feed.xml', entries=result, pagination=pagination)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(result))
return render_xml_template('feed.xml', entries=result, pagination=pagination)
@opds.route("/opds/download/<book_id>/<book_format>/")
@requires_basic_auth_if_no_ano
@download_required
def opds_download_link(book_id, book_format):
return get_download_link(book_id,book_format)
return get_download_link(book_id, book_format.lower())
@opds.route("/ajax/book/<string:uuid>/<library>")
@opds.route("/ajax/book/<string:uuid>")
@opds.route("/ajax/book/<string:uuid>", defaults={'library': ""})
@requires_basic_auth_if_no_ano
def get_metadata_calibre_companion(uuid, library):
entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first()
@@ -340,16 +387,17 @@ def get_metadata_calibre_companion(uuid, library):
def feed_search(term):
if term:
term = term.strip().lower()
entries = get_search_results( term)
entries = get_search_results(term)
entriescount = len(entries) if len(entries) > 0 else 1
pagination = Pagination(1, entriescount, entriescount)
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
else:
return render_xml_template('feed.xml', searchterm="")
def check_auth(username, password):
if sys.version_info.major == 3:
username=username.encode('windows-1252')
username = username.encode('windows-1252')
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) ==
username.decode('utf-8').lower()).first()
return bool(user and check_password_hash(str(user.password), password))
@@ -363,13 +411,14 @@ def authenticate():
def render_xml_template(*args, **kwargs):
#ToDo: return time in current timezone similar to %z
# ToDo: return time in current timezone similar to %z
currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
xml = render_template(current_time=currtime, instance=config.config_calibre_web_title, *args, **kwargs)
response = make_response(xml)
response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
return response
@opds.route("/opds/thumb_240_240/<book_id>")
@opds.route("/opds/cover_240_240/<book_id>")
@opds.route("/opds/cover_90_90/<book_id>")
@@ -378,13 +427,15 @@ def render_xml_template(*args, **kwargs):
def feed_get_cover(book_id):
return get_book_cover(book_id)
@opds.route("/opds/readbooks")
@requires_basic_auth_if_no_ano
def feed_read_books():
off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
return render_xml_template('feed.xml', entries=result, pagination=pagination)
@opds.route("/opds/unreadbooks")
@requires_basic_auth_if_no_ano
def feed_unread_books():
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Flask License
+7 -1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Flask License
@@ -60,10 +59,13 @@ class ReverseProxied(object):
def __init__(self, application):
self.app = application
self.proxied = False
def __call__(self, environ, start_response):
self.proxied = False
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
if script_name:
self.proxied = True
environ['SCRIPT_NAME'] = script_name
path_info = environ.get('PATH_INFO', '')
if path_info and path_info.startswith(script_name):
@@ -76,3 +78,7 @@ class ReverseProxied(object):
if servr:
environ['HTTP_HOST'] = servr
return self.app(environ, start_response)
@property
def is_proxied(self):
return self.proxied
Executable → Regular
+11 -5
View File
@@ -43,7 +43,6 @@ from . import logger
log = logger.create()
def _readable_listen_address(address, port):
if ':' in address:
address = "[" + address + "]"
@@ -84,7 +83,8 @@ class WebServer(object):
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
self.ssl_args = dict(certfile=certfile_path, keyfile=keyfile_path)
else:
log.warning('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl.')
log.warning('The specified paths for the ssl certificate file and/or key file seem to be broken. '
'Ignoring ssl.')
log.warning('Cert path: %s', certfile_path)
log.warning('Key path: %s', keyfile_path)
@@ -146,7 +146,7 @@ class WebServer(object):
self.unix_socket_file = None
def _start_tornado(self):
if os.name == 'nt':
if os.name == 'nt' and sys.version_info > (3, 7):
import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port))
@@ -156,7 +156,7 @@ class WebServer(object):
max_buffer_size=209700000,
ssl_options=self.ssl_args)
http_server.listen(self.listen_port, self.listen_address)
self.wsgiserver = IOLoop.instance()
self.wsgiserver = IOLoop.current()
self.wsgiserver.start()
# wait for stop signal
self.wsgiserver.close(True)
@@ -171,12 +171,15 @@ class WebServer(object):
except Exception as ex:
log.error("Error starting server: %s", ex)
print("Error starting server: %s" % ex)
self.stop()
return False
finally:
self.wsgiserver = None
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")
@@ -191,10 +194,13 @@ class WebServer(object):
self.stop()
def stop(self, restart=False):
from . import updater_thread
updater_thread.stop()
log.info("webserver stop (restart=%s)", restart)
self.restart = restart
if self.wsgiserver:
if _GEVENT:
self.wsgiserver.close()
else:
self.wsgiserver.add_callback(self.wsgiserver.stop)
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 shavitmichael, 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 sys
from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__
from datetime import datetime
try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from flask import json
from .. import logger as log
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())
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
def to_epoch_timestamp(datetime_object):
return (datetime_object - datetime(1970, 1, 1)).total_seconds()
def get_datetime_from_json(json_object, field_name):
try:
return datetime.utcfromtimestamp(json_object[field_name])
except KeyError:
return datetime.min
class SyncToken:
""" The SyncToken is used to persist state accross requests.
When serialized over the response headers, the Kobo device will propagate the token onto following
requests to the service. As an example use-case, the SyncToken is used to detect books that have been added
to the library since the last time the device synced to the server.
Attributes:
books_last_created: Datetime representing the newest book that the device knows about.
books_last_modified: Datetime representing the last modified book that the device knows about.
"""
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
VERSION = "1-1-0"
LAST_MODIFIED_ADDED_VERSION = "1-1-0"
MIN_VERSION = "1-0-0"
token_schema = {
"type": "object",
"properties": {"version": {"type": "string"}, "data": {"type": "object"}, },
}
# This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
# A potential solution might be to keep a list of all known book uuids in the token, and look for any missing
# from the db.
data_schema_v1 = {
"type": "object",
"properties": {
"raw_kobo_store_token": {"type": "string"},
"books_last_modified": {"type": "string"},
"books_last_created": {"type": "string"},
"archive_last_modified": {"type": "string"},
"reading_state_last_modified": {"type": "string"},
"tags_last_modified": {"type": "string"},
},
}
def __init__(
self,
raw_kobo_store_token="",
books_last_created=datetime.min,
books_last_modified=datetime.min,
archive_last_modified=datetime.min,
reading_state_last_modified=datetime.min,
tags_last_modified=datetime.min,
):
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
@staticmethod
def from_headers(headers):
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
if sync_token_header == "":
return SyncToken()
# On the first sync from a Kobo device, we may receive the SyncToken
# from the official Kobo store. Without digging too deep into it, that
# token is of the form [b64encoded blob].[b64encoded blob 2]
if "." in sync_token_header:
return SyncToken(raw_kobo_store_token=sync_token_header)
try:
sync_token_json = json.loads(
b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4))
)
validate(sync_token_json, SyncToken.token_schema)
if sync_token_json["version"] < SyncToken.MIN_VERSION:
raise ValueError
data_json = sync_token_json["data"]
validate(sync_token_json, SyncToken.data_schema_v1)
except (exceptions.ValidationError, ValueError):
log.error("Sync token contents do not follow the expected json schema.")
return SyncToken()
raw_kobo_store_token = data_json["raw_kobo_store_token"]
try:
books_last_modified = get_datetime_from_json(data_json, "books_last_modified")
books_last_created = get_datetime_from_json(data_json, "books_last_created")
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")
except TypeError:
log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
return SyncToken(
raw_kobo_store_token=raw_kobo_store_token,
books_last_created=books_last_created,
books_last_modified=books_last_modified,
archive_last_modified=archive_last_modified,
reading_state_last_modified=reading_state_last_modified,
tags_last_modified=tags_last_modified
)
def set_kobo_store_header(self, store_headers):
store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token)
def merge_from_store_response(self, store_response):
self.raw_kobo_store_token = store_response.headers.get(
SyncToken.SYNC_TOKEN_HEADER, ""
)
def to_headers(self, headers):
headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
def build_sync_token(self):
token = {
"version": SyncToken.VERSION,
"data": {
"raw_kobo_store_token": self.raw_kobo_store_token,
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
"books_last_created": to_epoch_timestamp(self.books_last_created),
"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)
},
}
return b64encode_json(token)
+13 -4
View File
@@ -26,13 +26,22 @@ log = logger.create()
try: from . import goodreads_support
except ImportError as err:
log.debug("cannot import goodreads, showing authors-metadata will not work: %s", err)
log.debug("Cannot import goodreads, showing authors-metadata will not work: %s", err)
goodreads_support = None
try: from . import simpleldap as ldap
try:
from . import simpleldap as ldap
from .simpleldap import ldapVersion
except ImportError as err:
log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err)
log.debug("Cannot import simpleldap, logging in with ldap will not work: %s", err)
ldap = None
ldapVersion = None
try:
from . import SyncToken as SyncToken
kobo = True
except ImportError as err:
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
kobo = None
SyncToken = None
+53 -18
View File
@@ -23,6 +23,10 @@ from flask_simpleldap import LDAP, LDAPException
from .. import constants, logger
try:
from ldap.pkginfo import __version__ as ldapVersion
except ImportError:
pass
log = logger.create()
_ldap = LDAP()
@@ -34,44 +38,75 @@ def init_app(app, config):
app.config['LDAP_HOST'] = config.config_ldap_provider_url
app.config['LDAP_PORT'] = config.config_ldap_port
app.config['LDAP_SCHEMA'] = config.config_ldap_schema
app.config['LDAP_USERNAME'] = config.config_ldap_user_object.replace('%s', config.config_ldap_serv_username)\
+ ',' + config.config_ldap_dn
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password)
app.config['LDAP_REQUIRE_CERT'] = bool(config.config_ldap_require_cert)
if config.config_ldap_require_cert:
if config.config_ldap_encryption == 2:
app.config['LDAP_SCHEMA'] = 'ldaps'
else:
app.config['LDAP_SCHEMA'] = 'ldap'
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if config.config_ldap_serv_password is None:
config.config_ldap_serv_password = ''
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password)
else:
app.config['LDAP_PASSWORD'] = base64.b64decode("")
app.config['LDAP_USERNAME'] = config.config_ldap_serv_username
else:
app.config['LDAP_USERNAME'] = ""
app.config['LDAP_PASSWORD'] = base64.b64decode("")
if bool(config.config_ldap_cert_path):
app.config['LDAP_REQUIRE_CERT'] = True
app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path
app.config['LDAP_BASE_DN'] = config.config_ldap_dn
app.config['LDAP_USER_OBJECT_FILTER'] = config.config_ldap_user_object
app.config['LDAP_USE_SSL'] = bool(config.config_ldap_use_ssl)
app.config['LDAP_USE_TLS'] = bool(config.config_ldap_use_tls)
app.config['LDAP_USE_TLS'] = bool(config.config_ldap_encryption == 1)
app.config['LDAP_USE_SSL'] = bool(config.config_ldap_encryption == 2)
app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap)
app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
# app.config['LDAP_CUSTOM_OPTIONS'] = {'OPT_NETWORK_TIMEOUT': 10}
_ldap.init_app(app)
def get_object_details(user=None, group=None, query_filter=None, dn_only=False):
return _ldap.get_object_details(user, group, query_filter, dn_only)
def bind():
return _ldap.bind()
def get_group_members(group):
return _ldap.get_group_members(group)
def basic_auth_required(func):
return _ldap.basic_auth_required(func)
def bind_user(username, password):
# ulf= _ldap.get_object_details('admin')
'''Attempts a LDAP login.
:returns: True if login succeeded, False if login failed, None if server unavailable.
'''
try:
result = _ldap.bind_user(username, password)
log.debug("LDAP login '%s': %r", username, result)
return result is not None
if _ldap.get_object_details(username):
result = _ldap.bind_user(username, password)
log.debug("LDAP login '%s': %r", username, result)
return result is not None, None
return None, None # User not found
except (TypeError, AttributeError, KeyError) as ex:
error = ("LDAP bind_user: %s" % ex)
return None, error
except LDAPException as ex:
if ex.message == 'Invalid credentials':
log.info("LDAP login '%s' failed: %s", username, ex)
return False
error = ("LDAP admin login failed")
return None, error
if ex.message == "Can't contact LDAP server":
log.warning('LDAP Server down: %s', ex)
return None
# log.warning('LDAP Server down: %s', ex)
error = ('LDAP Server down: %s' % ex)
return None, error
else:
log.warning('LDAP Server error: %s', ex.message)
return None
error = ('LDAP Server error: %s' % ex.message)
return None, error
+137 -87
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
@@ -22,11 +21,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from datetime import datetime
from flask import Blueprint, request, flash, redirect, url_for
from flask_babel import gettext as _
from flask_login import login_required, current_user
from sqlalchemy.sql.expression import func, or_, and_
from sqlalchemy.sql.expression import func
from . import logger, ub, searched_ids, db
from .web import render_title_template
@@ -37,37 +37,49 @@ shelf = Blueprint('shelf', __name__)
log = logger.create()
def check_shelf_edit_permissions(cur_shelf):
if not cur_shelf.is_public and not cur_shelf.user_id == int(current_user.id):
log.error("User %s not allowed to edit shelf %s", current_user, cur_shelf)
return False
if cur_shelf.is_public and not current_user.role_edit_shelfs():
log.info("User %s not allowed to edit public shelves", current_user)
return False
return True
def check_shelf_view_permissions(cur_shelf):
if cur_shelf.is_public:
return True
if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
log.error("User is unauthorized to view non-public shelf: %s", cur_shelf)
return False
return True
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
@login_required
def add_to_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)
if not request.is_xhr:
if not xhr:
flash(_(u"Invalid shelf specified"), category="error")
return redirect(url_for('web.index'))
return "Invalid shelf specified", 400
if not shelf.is_public and not shelf.user_id == int(current_user.id):
log.error("User %s not allowed to add a book to %s", current_user, shelf)
if not request.is_xhr:
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")
return redirect(url_for('web.index'))
return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403
if shelf.is_public and not current_user.role_edit_shelfs():
log.info("User %s not allowed to edit public shelves", current_user)
if not request.is_xhr:
flash(_(u"You are not allowed to edit public shelves"), category="error")
return redirect(url_for('web.index'))
return "User is not allowed to edit public shelves", 403
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
ub.BookShelf.book_id == book_id).first()
ub.BookShelf.book_id == book_id).first()
if book_in_shelf:
log.error("Book %s is already part of %s", book_id, shelf)
if not request.is_xhr:
if not xhr:
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
return redirect(url_for('web.index'))
return "Book is already part of the shelf: %s" % shelf.name, 400
@@ -78,10 +90,11 @@ def add_to_shelf(shelf_id, book_id):
else:
maxOrder = maxOrder[0]
ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1)
ub.session.add(ins)
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1))
shelf.last_modified = datetime.utcnow()
ub.session.merge(shelf)
ub.session.commit()
if not request.is_xhr:
if not xhr:
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"])
@@ -99,16 +112,10 @@ def search_to_shelf(shelf_id):
flash(_(u"Invalid shelf specified"), category="error")
return redirect(url_for('web.index'))
if not shelf.is_public and not shelf.user_id == int(current_user.id):
log.error("User %s not allowed to add a book to %s", current_user, shelf)
if not check_shelf_edit_permissions(shelf):
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'))
if shelf.is_public and not current_user.role_edit_shelfs():
log.error("User %s not allowed to edit public shelves", current_user)
flash(_(u"User is not allowed to edit public shelves"), category="error")
return redirect(url_for('web.index'))
if current_user.id in searched_ids and searched_ids[current_user.id]:
books_for_shelf = list()
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
@@ -135,8 +142,9 @@ def search_to_shelf(shelf_id):
for book in books_for_shelf:
maxOrder = maxOrder + 1
ins = ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder)
ub.session.add(ins)
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
shelf.last_modified = datetime.utcnow()
ub.session.merge(shelf)
ub.session.commit()
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
else:
@@ -147,10 +155,11 @@ def search_to_shelf(shelf_id):
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>")
@login_required
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)
if not request.is_xhr:
if not xhr:
return redirect(url_for('web.index'))
return "Invalid shelf specified", 400
@@ -162,34 +171,35 @@ def remove_from_shelf(shelf_id, book_id):
# true 0 x 1
# false 0 x 0
if (not shelf.is_public and shelf.user_id == int(current_user.id)) \
or (shelf.is_public and current_user.role_edit_shelfs()):
if check_shelf_edit_permissions(shelf):
book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
ub.BookShelf.book_id == book_id).first()
if book_shelf is None:
log.error("Book %s already removed from %s", book_id, shelf)
if not request.is_xhr:
if not xhr:
return redirect(url_for('web.index'))
return "Book already removed from shelf", 410
ub.session.delete(book_shelf)
shelf.last_modified = datetime.utcnow()
ub.session.commit()
if not request.is_xhr:
if not xhr:
flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
return redirect(request.environ["HTTP_REFERER"])
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
return redirect(url_for('web.index'))
return "", 204
else:
log.error("User %s not allowed to remove a book from %s", current_user, shelf)
if not request.is_xhr:
if not xhr:
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'))
return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403
@shelf.route("/shelf/create", methods=["GET", "POST"])
@login_required
def create_shelf():
@@ -200,21 +210,37 @@ def create_shelf():
shelf.is_public = 1
shelf.name = to_save["title"]
shelf.user_id = int(current_user.id)
existing_shelf = ub.session.query(ub.Shelf).filter(
or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1),
(ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).first()
if existing_shelf:
flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error")
is_shelf_name_unique = False
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)) \
.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"]),
category="error")
else:
is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) &
(ub.Shelf.user_id == int(current_user.id)))\
.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"]),
category="error")
if is_shelf_name_unique:
try:
ub.session.add(shelf)
ub.session.commit()
flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success")
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
except Exception:
flash(_(u"There was an error"), category="error")
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate")
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Create a Shelf"), page="shelfcreate")
else:
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate")
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Create a Shelf"), page="shelfcreate")
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
@@ -223,14 +249,31 @@ def edit_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if request.method == "POST":
to_save = request.form.to_dict()
existing_shelf = ub.session.query(ub.Shelf).filter(
or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1),
(ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).filter(
ub.Shelf.id != shelf_id).first()
if existing_shelf:
flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error")
is_shelf_name_unique = False
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.id != shelf_id) \
.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"]),
category="error")
else:
is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) &
(ub.Shelf.user_id == int(current_user.id)))\
.filter(ub.Shelf.id != shelf_id)\
.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"]),
category="error")
if is_shelf_name_unique:
shelf.name = to_save["title"]
shelf.last_modified = datetime.utcnow()
if "is_public" in to_save:
shelf.is_public = 1
else:
@@ -245,55 +288,54 @@ def edit_shelf(shelf_id):
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
def delete_shelf_helper(cur_shelf):
if not cur_shelf or not check_shelf_edit_permissions(cur_shelf):
return
shelf_id = cur_shelf.id
ub.session.delete(cur_shelf)
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
ub.session.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id))
ub.session.commit()
log.info("successfully deleted %s", cur_shelf)
@shelf.route("/shelf/delete/<int:shelf_id>")
@login_required
def delete_shelf(shelf_id):
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
deleted = None
if current_user.role_admin():
deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete()
else:
if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \
or (cur_shelf.is_public and current_user.role_edit_shelfs()):
deleted = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
ub.Shelf.id == shelf_id),
and_(ub.Shelf.is_public == 1,
ub.Shelf.id == shelf_id))).delete()
if deleted:
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
ub.session.commit()
log.info("successfully deleted %s", cur_shelf)
delete_shelf_helper(cur_shelf)
return redirect(url_for('web.index'))
# @shelf.route("/shelfdown/<int:shelf_id>")
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
def show_shelf(shelf_type, shelf_id):
if current_user.is_anonymous:
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
else:
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
ub.Shelf.id == shelf_id),
and_(ub.Shelf.is_public == 1,
ub.Shelf.id == shelf_id))).first()
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
result = list()
# user is allowed to access shelf
if shelf:
if shelf and check_shelf_view_permissions(shelf):
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
.order_by(ub.BookShelf.order.asc()).all()
books_list = [ b.book_id for b in books_in_shelf]
result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all()
for book in books_in_shelf:
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).filter(common_filters()).first()
if cur_book:
result.append(cur_book)
else:
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
if not cur_book:
log.info('Not existing book %s in %s deleted', book.book_id, shelf)
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete()
ub.session.commit()
return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelf")
shelf=shelf, page="shelf")
else:
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
return redirect(url_for("web.index"))
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
@login_required
def order_shelf(shelf_id):
@@ -305,20 +347,28 @@ def order_shelf(shelf_id):
for book in books_in_shelf:
setattr(book, 'order', to_save[str(book.book_id)])
counter += 1
# if order diffrent from before -> shelf.last_modified = datetime.utcnow()
ub.session.commit()
if current_user.is_anonymous:
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
else:
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
ub.Shelf.id == shelf_id),
and_(ub.Shelf.is_public == 1,
ub.Shelf.id == shelf_id))).first()
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
result = list()
if shelf:
if shelf and check_shelf_view_permissions(shelf):
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
.order_by(ub.BookShelf.order.asc()).all()
books_list = [ b.book_id for b in books_in_shelf2]
result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all()
for book in books_in_shelf2:
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).filter(common_filters()).first()
if cur_book:
result.append({'title': cur_book.title,
'id': cur_book.id,
'author': cur_book.authors,
'series': cur_book.series,
'series_index': cur_book.series_index})
else:
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
result.append({'title': _('Hidden Book'),
'id': cur_book.id,
'author': [],
'series': []})
return render_title_template('shelf_order.html', entries=result,
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelforder")
+1 -1
View File
File diff suppressed because one or more lines are too long
+13 -15
View File
@@ -4,7 +4,7 @@ body {
overflow-y: auto;
color: white;
font-family: sans-serif;
margin: 0px;
margin: 0;
}
#main {
@@ -13,7 +13,7 @@ body {
}
.view {
padding-top:0px;
padding-top: 0;
}
#sidebar a,
@@ -34,18 +34,18 @@ body {
cursor: pointer;
padding: 4px;
transition: all .2s ease;
transition: all 0.2s ease;
}
#sidebar a:hover,
#sidebar a:focus {
outline: none;
box-shadow: 0px 2px 8px 1px black;
box-shadow: 0 2px 8px 1px black;
}
#sidebar a.active,
#sidebar a.active img + span {
background-color: #45B29D;
background-color: #45B29D;
}
#sidebar li img {
@@ -79,7 +79,6 @@ body {
font-size: 10px;
line-height: 10px;
text-align: right;
transition: min-height 150ms ease-in-out;
}
@@ -92,18 +91,17 @@ body {
top: 0;
left: 0;
bottom: 0;
transition: width 150ms ease-in-out;
}
#progress .bar-load {
color: #000;
background-color: #CCC;
background-color: #ccc;
}
#progress .bar-read {
color: #FFF;
background-color: #45B29D;
color: #fff;
background-color: #45b29d;
}
#progress .text {
@@ -152,7 +150,8 @@ body {
max-width: 70%;
}
th, td {
th,
td {
padding: 5px;
}
@@ -206,18 +205,17 @@ th {
}
.dark-theme #titlebar {
color: #DDD;
color: #ddd;
}
.dark-theme #titlebar a:active {
color: #FFF;
color: #fff;
}
.dark-theme #progress .bar-read {
background-color: red;
}
.dark-theme .overlay {
background-color: rgba(0,0,0,0.8);
background-color: rgba(0, 0, 0, 0.8);
}
+88 -28
View File
@@ -230,36 +230,46 @@
z-index: 200;
max-width: 20em;
background-color: #FFFF99;
box-shadow: 0px 2px 5px #333;
box-shadow: 0px 2px 5px #888;
border-radius: 2px;
padding: 0.6em;
padding: 6px;
margin-left: 5px;
cursor: pointer;
font: message-box;
font-size: 9px;
word-wrap: break-word;
}
.annotationLayer .popup > * {
font-size: 9px;
}
.annotationLayer .popup h1 {
font-size: 1em;
border-bottom: 1px solid #000000;
margin: 0;
padding-bottom: 0.2em;
display: inline-block;
}
.annotationLayer .popup span {
display: inline-block;
margin-left: 5px;
}
.annotationLayer .popup p {
margin: 0;
padding-top: 0.2em;
border-top: 1px solid #333;
margin-top: 2px;
padding-top: 2px;
}
.annotationLayer .highlightAnnotation,
.annotationLayer .underlineAnnotation,
.annotationLayer .squigglyAnnotation,
.annotationLayer .strikeoutAnnotation,
.annotationLayer .freeTextAnnotation,
.annotationLayer .lineAnnotation svg line,
.annotationLayer .squareAnnotation svg rect,
.annotationLayer .circleAnnotation svg ellipse,
.annotationLayer .polylineAnnotation svg polyline,
.annotationLayer .polygonAnnotation svg polygon,
.annotationLayer .caretAnnotation,
.annotationLayer .inkAnnotation svg polyline,
.annotationLayer .stampAnnotation,
.annotationLayer .fileAttachmentAnnotation {
@@ -279,8 +289,9 @@
overflow: visible;
border: 9px solid transparent;
background-clip: content-box;
-o-border-image: url(images/shadow.png) 9 9 repeat;
border-image: url(images/shadow.png) 9 9 repeat;
-webkit-border-image: url(images/shadow.png) 9 9 repeat;
-o-border-image: url(images/shadow.png) 9 9 repeat;
border-image: url(images/shadow.png) 9 9 repeat;
background-color: white;
}
@@ -543,15 +554,20 @@ select {
z-index: 100;
border-top: 1px solid #333;
transition-duration: 200ms;
transition-timing-function: ease;
-webkit-transition-duration: 200ms;
transition-duration: 200ms;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
}
html[dir='ltr'] #sidebarContainer {
-webkit-transition-property: left;
transition-property: left;
left: -200px;
left: calc(-1 * var(--sidebar-width));
}
html[dir='rtl'] #sidebarContainer {
-webkit-transition-property: right;
transition-property: right;
right: -200px;
right: calc(-1 * var(--sidebar-width));
@@ -563,7 +579,8 @@ html[dir='rtl'] #sidebarContainer {
#outerContainer.sidebarResizing #sidebarContainer {
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
transition-duration: 0s;
-webkit-transition-duration: 0s;
transition-duration: 0s;
/* Prevent e.g. the thumbnails being selected when the sidebar is resized. */
-webkit-user-select: none;
-moz-user-select: none;
@@ -620,8 +637,10 @@ html[dir='rtl'] #sidebarContent {
outline: none;
}
#viewerContainer:not(.pdfPresentationMode) {
transition-duration: 200ms;
transition-timing-function: ease;
-webkit-transition-duration: 200ms;
transition-duration: 200ms;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
}
html[dir='ltr'] #viewerContainer {
box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05);
@@ -632,15 +651,18 @@ html[dir='rtl'] #viewerContainer {
#outerContainer.sidebarResizing #viewerContainer {
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
transition-duration: 0s;
-webkit-transition-duration: 0s;
transition-duration: 0s;
}
html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
-webkit-transition-property: left;
transition-property: left;
left: 200px;
left: var(--sidebar-width);
}
html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
-webkit-transition-property: right;
transition-property: right;
right: 200px;
right: var(--sidebar-width);
@@ -662,6 +684,8 @@ html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentatio
width: 100%;
height: 32px;
background-color: #424242; /* fallback */
background-image: url(images/texture.png),
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,30%,.99)), to(hsla(0,0%,25%,.95)));
background-image: url(images/texture.png),
linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
}
@@ -697,6 +721,8 @@ html[dir='rtl'] #sidebarResizer {
position: relative;
height: 32px;
background-color: #474747; /* fallback */
background-image: url(images/texture.png),
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,32%,.99)), to(hsla(0,0%,27%,.95)));
background-image: url(images/texture.png),
linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
}
@@ -733,6 +759,7 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar {
height: 100%;
background-color: #ddd;
overflow: hidden;
-webkit-transition: width 200ms;
transition: width 200ms;
}
@@ -748,6 +775,7 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar {
#loadingBar .progress.indeterminate {
background-color: #999;
-webkit-transition: none;
transition: none;
}
@@ -815,6 +843,9 @@ html[dir='rtl'] .findbar {
#findInput::-webkit-input-placeholder {
color: hsl(0, 0%, 75%);
}
#findInput::-moz-placeholder {
font-style: italic;
}
#findInput:-ms-input-placeholder {
font-style: italic;
}
@@ -1006,6 +1037,7 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton {
.splitToolbarButton.toggled > .toolbarButton,
.toolbarButton.textButton {
background-color: hsla(0,0%,0%,.12);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
background-clip: padding-box;
border: 1px solid hsla(0,0%,0%,.35);
@@ -1013,9 +1045,12 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton {
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
0 0 1px hsla(0,0%,100%,.15) inset,
0 1px 0 hsla(0,0%,100%,.05);
-webkit-transition-property: background-color, border-color, box-shadow;
transition-property: background-color, border-color, box-shadow;
transition-duration: 150ms;
transition-timing-function: ease;
-webkit-transition-duration: 150ms;
transition-duration: 150ms;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
}
.splitToolbarButton > .toolbarButton:hover,
@@ -1072,9 +1107,12 @@ html[dir='rtl'] .splitToolbarButtonSeparator {
padding: 12px 0;
margin: 1px 0;
box-shadow: 0 0 0 1px hsla(0,0%,100%,.03);
-webkit-transition-property: padding;
transition-property: padding;
transition-duration: 10ms;
transition-timing-function: ease;
-webkit-transition-duration: 10ms;
transition-duration: 10ms;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
}
.toolbarButton,
@@ -1094,9 +1132,12 @@ html[dir='rtl'] .splitToolbarButtonSeparator {
user-select: none;
/* Opera does not support user-select, use <... unselectable="on"> instead */
cursor: default;
-webkit-transition-property: background-color, border-color, box-shadow;
transition-property: background-color, border-color, box-shadow;
transition-duration: 150ms;
transition-timing-function: ease;
-webkit-transition-duration: 150ms;
transition-duration: 150ms;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
}
html[dir='ltr'] .toolbarButton,
@@ -1117,6 +1158,7 @@ html[dir='rtl'] .dropdownToolbarButton {
.secondaryToolbarButton:hover,
.secondaryToolbarButton:focus {
background-color: hsla(0,0%,0%,.12);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
background-clip: padding-box;
border: 1px solid hsla(0,0%,0%,.35);
@@ -1131,28 +1173,36 @@ html[dir='rtl'] .dropdownToolbarButton {
.dropdownToolbarButton:hover:active,
.secondaryToolbarButton:hover:active {
background-color: hsla(0,0%,0%,.2);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45);
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
0 0 1px hsla(0,0%,0%,.2) inset,
0 1px 0 hsla(0,0%,100%,.05);
-webkit-transition-property: background-color, border-color, box-shadow;
transition-property: background-color, border-color, box-shadow;
transition-duration: 10ms;
transition-timing-function: linear;
-webkit-transition-duration: 10ms;
transition-duration: 10ms;
-webkit-transition-timing-function: linear;
transition-timing-function: linear;
}
.toolbarButton.toggled,
.splitToolbarButton.toggled > .toolbarButton.toggled,
.secondaryToolbarButton.toggled {
background-color: hsla(0,0%,0%,.3);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5);
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
0 0 1px hsla(0,0%,0%,.2) inset,
0 1px 0 hsla(0,0%,100%,.05);
-webkit-transition-property: background-color, border-color, box-shadow;
transition-property: background-color, border-color, box-shadow;
transition-duration: 10ms;
transition-timing-function: linear;
-webkit-transition-duration: 10ms;
transition-duration: 10ms;
-webkit-transition-timing-function: linear;
transition-timing-function: linear;
}
.toolbarButton.toggled:hover:active,
@@ -1493,6 +1543,7 @@ html[dir='rtl'] .verticalToolbarSeparator {
border: 1px solid transparent;
border-radius: 2px;
background-color: hsla(0,0%,100%,.09);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
background-clip: padding-box;
border: 1px solid hsla(0,0%,0%,.35);
@@ -1503,9 +1554,12 @@ html[dir='rtl'] .verticalToolbarSeparator {
font-size: 12px;
line-height: 14px;
outline-style: none;
-webkit-transition-property: background-color, border-color, box-shadow;
transition-property: background-color, border-color, box-shadow;
transition-duration: 150ms;
transition-timing-function: ease;
-webkit-transition-duration: 150ms;
transition-duration: 150ms;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
}
.toolbarField[type=checkbox] {
@@ -1619,6 +1673,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing > .thumbnailImage,
a:focus > .thumbnail > .thumbnailSelectionRing,
.thumbnail:hover > .thumbnailSelectionRing {
background-color: hsla(0,0%,100%,.15);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
background-clip: padding-box;
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
@@ -1634,6 +1689,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing,
.thumbnail.selected > .thumbnailSelectionRing {
background-color: hsla(0,0%,100%,.3);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
background-clip: padding-box;
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
@@ -1755,6 +1811,7 @@ html[dir='rtl'] .outlineItemToggler::before {
.outlineItem > a:hover,
.attachmentsItem > button:hover {
background-color: hsla(0,0%,100%,.02);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
background-clip: padding-box;
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
@@ -1766,6 +1823,7 @@ html[dir='rtl'] .outlineItemToggler::before {
.outlineItem.selected {
background-color: hsla(0,0%,100%,.08);
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
background-clip: padding-box;
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
@@ -1850,6 +1908,8 @@ html[dir='rtl'] .outlineItemToggler::before {
font-size: 12px;
line-height: 14px;
background-color: #474747; /* fallback */
background-image: url(images/texture.png),
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,32%,.99)), to(hsla(0,0%,27%,.95)));
background-image: url(images/texture.png),
linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08),
+73 -76
View File
@@ -1,92 +1,89 @@
.sm2-bar-ui {
font-size: 20px;
}
font-size: 20px;
}
.sm2-bar-ui.compact {
max-width: 90%;
}
.sm2-bar-ui.compact {
max-width: 90%;
}
.sm2-progress .sm2-progress-ball {
width: .5333em;
height: 1.9333em;
border-radius: 0em;
}
.sm2-progress .sm2-progress-ball {
width: 0.5333em;
height: 1.9333em;
border-radius: 0;
}
.sm2-progress .sm2-progress-track {
height: 0.15em;
background: white;
}
.sm2-progress .sm2-progress-track {
height: 0.15em;
background: white;
}
.sm2-bar-ui .sm2-main-controls,
.sm2-bar-ui .sm2-playlist-drawer {
background-color: transparent;
}
.sm2-bar-ui .sm2-main-controls,
.sm2-bar-ui .sm2-playlist-drawer {
background-color: transparent;
}
.sm2-bar-ui .sm2-inline-texture {
background: transparent;
}
.sm2-bar-ui .sm2-inline-texture {
background: transparent;
}
.rating .glyphicon-star {
color: gray;
}
.rating .glyphicon-star {
color: gray;
}
.rating .glyphicon-star.good {
color: white;
}
.rating .glyphicon-star.good {
color: white;
}
body {
overflow: hidden;
background: #272B30;
color: #aaa;
}
body {
overflow: hidden;
background: #272b30;
color: #aaa;
}
#main {
position: absolute;
width: 100%;
height: 100%;
}
#main {
position: absolute;
width: 100%;
height: 100%;
}
#area {
width: 80%;
height: 80%;
margin: 5% auto;
max-width: 1250px;
}
#area {
width: 80%;
height: 80%;
margin: 5% auto;
max-width: 1250px;
overflow: hidden;
}
#area iframe {
border: none;
}
#area iframe {
border: none;
}
#prev {
left: 40px;
}
#prev {
left: 40px;
}
#next {
right: 40px;
}
#next {
right: 40px;
}
xmp,
pre,
plaintext {
display: block;
font-family: -moz-fixed;
white-space: pre;
margin: 1em 0;
}
xmp,
pre,
plaintext {
display: block;
font-family: -moz-fixed;
white-space: pre;
margin: 1em 0;
}
#area {
overflow: hidden;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: -moz-fixed;
column-count: 2;
-webkit-columns: 2;
-moz-columns: 2;
column-gap: 20px;
-moz-column-gap: 20px;
-webkit-column-gap: 20px;
position: relative;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: -moz-fixed;
column-count: 2;
-webkit-columns: 2;
-moz-columns: 2;
column-gap: 20px;
-moz-column-gap: 20px;
-webkit-column-gap: 20px;
position: relative;
}
+76 -122
View File
@@ -1,10 +1,11 @@
@font-face {
font-family: 'fontello';
src: url('fonts/fontello.eot?60518104');
src: url('fonts/fontello.eot?60518104#iefix') format('embedded-opentype'),
url('fonts/fontello.woff?60518104') format('woff'),
url('fonts/fontello.ttf?60518104') format('truetype'),
url('fonts/fontello.svg?60518104#fontello') format('svg');
src:
url('fonts/fontello.eot?60518104#iefix') format('embedded-opentype'),
url('fonts/fontello.woff?60518104') format('woff'),
url('fonts/fontello.ttf?60518104') format('truetype'),
url('fonts/fontello.svg?60518104#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -22,17 +23,15 @@ body {
border-radius: 5px;
background: #fff;
overflow: hidden;
-webkit-transition: -webkit-transform .4s, width .2s;
-moz-transition: -webkit-transform .4s, width .2s;
-ms-transition: -webkit-transform .4s, width .2s;
-moz-box-shadow: inset 0 0 50px rgba(0,0,0,.1);
-webkit-box-shadow: inset 0 0 50px rgba(0,0,0,.1);
-ms-box-shadow: inset 0 0 50px rgba(0,0,0,.1);
box-shadow: inset 0 0 50px rgba(0,0,0,.1);
-webkit-transition: -webkit-transform 0.4s, width 0.2s;
-moz-transition: -webkit-transform 0.4s, width 0.2s;
-ms-transition: -webkit-transform 0.4s, width 0.2s;
-moz-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
-webkit-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
-ms-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
}
#titlebar {
height: 8%;
min-height: 20px;
@@ -42,11 +41,11 @@ body {
color: #4f4f4f;
font-weight: 100;
font-family: Georgia, "Times New Roman", Times, serif;
opacity: .5;
opacity: 0.5;
text-align: center;
-webkit-transition: opacity .5s;
-moz-transition: opacity .5s;
-ms-transition: opacity .5s;
-webkit-transition: opacity 0.5s;
-moz-transition: opacity 0.5s;
-ms-transition: opacity 0.5s;
z-index: 10;
}
@@ -60,7 +59,7 @@ body {
line-height: 20px;
overflow: hidden;
display: inline-block;
opacity: .5;
opacity: 0.5;
padding: 4px;
border-radius: 4px;
}
@@ -70,35 +69,27 @@ body {
}
#titlebar a:hover {
opacity: .8;
border: 1px rgba(0,0,0,.2) solid;
opacity: 0.8;
border: 1px rgba(0, 0, 0, 0.2) solid;
padding: 3px;
}
#titlebar a:active {
opacity: 1;
color: rgba(0,0,0,.6);
/* margin: 1px -1px -1px 1px; */
-moz-box-shadow: inset 0 0 6px rgba(155,155,155,.8);
-webkit-box-shadow: inset 0 0 6px rgba(155,155,155,.8);
-ms-box-shadow: inset 0 0 6px rgba(155,155,155,.8);
box-shadow: inset 0 0 6px rgba(155,155,155,.8);
color: rgba(0, 0, 0, 0.6);
-moz-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
-webkit-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
-ms-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
}
#book-title {
font-weight: 600;
}
#title-seperator {
display: none;
}
#book-title { font-weight: 600; }
#title-seperator { display: none; }
#viewer {
width: 80%;
height: 80%;
/* margin-left: 10%; */
margin: 0 auto;
/* max-width: 1250px; */
z-index: 2;
position: relative;
overflow: hidden;
@@ -108,14 +99,16 @@ body {
border: none;
}
#left,#prev {
#left,
#prev {
left: 40px;
padding-right:80px;
padding-right: 80px;
}
#right,#next {
#right,
#next {
right: 40px;
padding-left:80px;
padding-left: 80px;
}
.arrow {
@@ -148,24 +141,20 @@ body {
#sidebar {
background: #6b6b6b;
position: absolute;
/* left: -260px; */
/* -webkit-transform: translate(-260px, 0);
-moz-transform: translate(-260px, 0); */
top: 0;
min-width: 300px;
width: 25%;
height: 100%;
-webkit-transition: -webkit-transform .5s;
-moz-transition: -moz-transform .5s;
-ms-transition: -moz-transform .5s;
-webkit-transition: -webkit-transform 0.5s;
-moz-transition: -moz-transform 0.5s;
-ms-transition: -moz-transform 0.5s;
overflow: hidden;
}
#sidebar.open {
/* left: 0; */
/* -webkit-transform: translate(0, 0);
-moz-transform: translate(0, 0); */
/* left: 0; */
/* -webkit-transform: translate(0, 0);
-moz-transform: translate(0, 0); */
}
#main.closed {
@@ -192,10 +181,10 @@ body {
width: 100%;
padding: 13px 0;
height: 14px;
-moz-box-shadow: 0px 1px 3px rgba(0,0,0,.6);
-webkit-box-shadow: 0px 1px 3px rgba(0,0,0,.6);
-ms-box-shadow: 0px 1px 3px rgba(0,0,0,.6);
box-shadow: 0px 1px 3px rgba(0,0,0,.6);
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
-ms-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}
#opener {
@@ -203,19 +192,13 @@ body {
float: left;
}
/* #opener #slider {
width: 25px;
} */
#metainfo {
display: inline-block;
text-align: center;
max-width: 80%;
}
#title-controls {
float: right;
}
#title-controls { float: right; }
#panels a {
visibility: hidden;
@@ -227,22 +210,17 @@ body {
margin-left: 6px;
}
#panels a::before {
visibility: visible;
}
#panels a:hover {
color: #AAA;
}
#panels a::before { visibility: visible; }
#panels a:hover { color: #aaa; }
#panels a:active {
color: #AAA;
color: #aaa;
margin: 1px 0 -1px 6px;
}
#panels a.active,
#panels a.active:hover {
color: #AAA;
color: #aaa;
}
#searchBox {
@@ -250,28 +228,11 @@ body {
float: left;
margin-left: 10px;
margin-top: -1px;
/*
border-radius: 5px;
background: #9b9b9b;
float: left;
margin-left: 5px;
margin-top: -5px;
padding: 3px 10px;
color: #000;
border: none;
outline: none; */
}
input::-webkit-input-placeholder {
color: #454545;
}
input:-moz-placeholder {
color: #454545;
}
input:-ms-placeholder {
color: #454545;
}
input::-webkit-input-placeholder { color: #454545; }
input:-moz-placeholder { color: #454545; }
input:-ms-placeholder { color: #454545; }
#divider {
position: absolute;
@@ -307,13 +268,11 @@ input:-ms-placeholder {
width: 25%;
height: 100%;
visibility: hidden;
-webkit-transition: visibility 0 ease .5s;
-moz-transition: visibility 0 ease .5s;
-ms-transition: visibility 0 ease .5s;
-webkit-transition: visibility 0 ease 0.5s;
-moz-transition: visibility 0 ease 0.5s;
-ms-transition: visibility 0 ease 0.5s;
}
#sidebar.open #tocView,
#sidebar.open #bookmarksView {
overflow-y: auto;
@@ -351,7 +310,7 @@ input:-ms-placeholder {
}
.list_item a {
color: #AAA;
color: #aaa;
text-decoration: none;
}
@@ -360,7 +319,7 @@ input:-ms-placeholder {
}
.list_item a.section {
font-size: .8em;
font-size: 0.8em;
}
.list_item.currentChapter > a,
@@ -370,7 +329,7 @@ input:-ms-placeholder {
/* #tocView li.openChapter > a, */
.list_item a:hover {
color: #E2E2E2;
color: #e2e2e2;
}
.list_item ul {
@@ -431,7 +390,7 @@ input:-ms-placeholder {
}
#searchResults a {
color: #AAA;
color: #aaa;
text-decoration: none;
}
@@ -447,11 +406,11 @@ input:-ms-placeholder {
}
#searchResults li > p {
color: #AAA;
color: #aaa;
}
#searchResults li a:hover {
color: #E2E2E2;
color: #e2e2e2;
}
#searchView.shown {
@@ -496,7 +455,7 @@ input:-ms-placeholder {
}
#note-text[disabled], #note-text[disabled="disabled"]{
opacity: .5;
opacity: 0.5;
}
#note-anchor {
@@ -505,30 +464,30 @@ input:-ms-placeholder {
}
#settingsPanel {
display:none;
display: none;
}
#settingsPanel h3 {
color:#f1f1f1;
font-family:Georgia, "Times New Roman", Times, serif;
margin-bottom:10px;
color: #f1f1f1;
font-family: Georgia, "Times New Roman", Times, serif;
margin-bottom: 10px;
}
#settingsPanel ul {
margin-top:60px;
list-style-type:none;
margin-top: 60px;
list-style-type: none;
}
#settingsPanel li {
font-size:1em;
color:#f1f1f1;
font-size: 1em;
color: #f1f1f1;
}
#settingsPanel .xsmall { font-size:x-small; }
#settingsPanel .small { font-size:small; }
#settingsPanel .medium { font-size:medium; }
#settingsPanel .large { font-size:large; }
#settingsPanel .xlarge { font-size:x-large; }
#settingsPanel .xsmall { font-size: x-small; }
#settingsPanel .small { font-size: small; }
#settingsPanel .medium { font-size: medium; }
#settingsPanel .large { font-size: large; }
#settingsPanel .xlarge { font-size: x-large; }
.highlight { background-color: yellow }
@@ -556,7 +515,7 @@ input:-ms-placeholder {
left: 0;
z-index: 1000;
opacity: 0;
background: rgba(255,255,255,0.8);
background: rgba(255, 255, 255, 0.8);
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
-ms-transition: all 0.3s;
@@ -589,7 +548,7 @@ input:-ms-placeholder {
font-size: 22px;
font-weight: 300;
opacity: 0.8;
background: rgba(0,0,0,0.1);
background: rgba(0, 0, 0, 0.1);
border-radius: 3px 3px 0 0;
}
@@ -823,23 +782,18 @@ and (orientation : landscape)
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
margin-right: 0.2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* you can be more comfortable with increased icons size */
font-size: 112%;
}
.icon-search:before { content: '\e807'; } /* '' */
.icon-resize-full-1:before { content: '\e804'; } /* '' */
.icon-cancel-circled2:before { content: '\e80f'; } /* '' */
+12 -15
View File
@@ -1,4 +1,5 @@
/* http://davidwalsh.name/css-tooltips */
/* base CSS element */
.popup {
background: #eee;
@@ -9,10 +10,8 @@
position: fixed;
max-width: 300px;
font-size: 12px;
display: none;
margin-left: 2px;
margin-top: 30px;
}
@@ -38,7 +37,7 @@
}
/* below */
.popup:before {
.popup::before {
position: absolute;
display: inline-block;
border-bottom: 10px solid #eee;
@@ -51,7 +50,7 @@
content: '';
}
.popup:after {
.popup::after {
position: absolute;
display: inline-block;
border-bottom: 9px solid #eee;
@@ -64,33 +63,31 @@
}
/* above */
.popup.above:before {
.popup.above::before {
border-bottom: none;
border-top: 10px solid #eee;
border-top-color: rgba(0, 0, 0, 0.2);
top: 100%;
}
.popup.above:after {
.popup.above::after {
border-bottom: none;
border-top: 9px solid #eee;
top: 100%;
}
.popup.left:before,
.popup.left:after
{
.popup.left::before,
.popup.left::after {
left: 20px;
}
.popup.right:before,
.popup.right:after
{
.popup.right::before,
.popup.right::after {
left: auto;
right: 20px;
}
.popup.show, .popup.on {
.popup.show,
.popup.on {
display: block;
}
}
+226 -88
View File
@@ -1,5 +1,20 @@
.tooltip.bottom .tooltip-inner{font-size:13px;font-family:Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;padding:3px 10px;border-radius:4px;background-color:#fff;-webkit-box-shadow:0 4px 10px 0 rgba(0,0,0,.35);box-shadow:0 4px 10px 0 rgba(0,0,0,.35);opacity:1;white-space:nowrap;margin-top:-16px!important;line-height:1.71428571;color:#ddd}
.tooltip.bottom .tooltip-inner {
font-size: 13px;
font-family: Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 3px 10px;
border-radius: 4px;
background-color: #fff;
-webkit-box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.35);
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.35);
opacity: 1;
white-space: nowrap;
margin-top: -16px !important;
line-height: 1.71428571;
color: #ddd;
}
@font-face {
font-family: 'Grand Hotel';
@@ -12,140 +27,263 @@ html.http-error {
margin: 0;
height: 100%;
}
.http-error body {
margin: 0;
height: 100%;
display: table;
width: 100%;
}
.http-error body > div {
display: table-cell;
vertical-align: middle;
text-align: center;
}
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
body { margin-bottom: 40px;}
a{color: #45b29d}a:hover{color: #444;}
.navigation .nav-head{text-transform:uppercase;color:#999;margin:20px 0}.navigation .nav-head:nth-child(1n+2){border-top:1px solid #ccc;padding-top:20px}
.navigation li a{color:#444;text-decoration:none;display:block;padding:10px}.navigation li a:hover{background:rgba(153,153,153,0.4);border-radius:5px}
.navigation li a span{margin-right:10px}
.navigation .create-shelf{margin:30px 0;font-size:12px;text-align:center}.navigation .create-shelf a{color:#fff;background:#45b29d;padding:10px 20px;border-radius:5px;text-decoration:none}
.container-fluid img{display:block;max-width:100%;height:auto}
.container-fluid .discover{margin-bottom:50px}
.container-fluid .new-books{border-top:1px solid #ccc}.container-fluid .new-books h2{margin:50px 0 0 0}
.container-fluid .book{margin-top:20px}.container-fluid .book .cover{height:225px;position:relative}.container-fluid .book .cover img{border:1px solid #fff;/*border-radius:7px;*/box-sizeing:border-box;height:100%;bottom:0;position:absolute;-webkit-box-shadow: 0 5px 8px -6px #777;-moz-box-shadow: 0 5px 8px -6px #777;box-shadow: 0 5px 8px -6px #777;}
.container-fluid .book .meta{margin-top:10px}.container-fluid .book .meta p{margin:0}
.container-fluid .book .meta .title{font-weight:bold;font-size:15px;color:#444}
.container-fluid .book .meta .author{font-size:12px;color:#999}
.container-fluid .book .meta .rating{margin-top:5px}.rating .glyphicon-star{color:#999}.rating .glyphicon-star.good{color:#45b29d}
.container-fluid .author .author-hidden, .container-fluid .author .author-hidden-divider {
display: none;
body {
background: #f2f2f2;
margin-bottom: 40px;
}
.navbar-brand{font-family: 'Grand Hotel', cursive; font-size: 35px; color: #45b29d !important;}
.more-stuff{margin-top: 20px; padding-top: 20px; border-top: 1px solid #ccc}
.more-stuff>li{margin-bottom: 10px;}
.navbar-collapse.in .navbar-nav{margin: 0;}
span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: text-top;}
.book-meta {padding-bottom: 20px;}
.book-meta .tags a {display: inline;}
.book-meta .identifiers a {display: inline;}
body h2 {
font-weight: normal;
color:#444;
}
a { color: #45b29d; }
.navigation .nav-head {
text-transform: uppercase;
color: #999;
margin: 20px 0;
}
.navigation .nav-head:nth-child(1n+2) {
border-top: 1px solid #ccc;
padding-top: 20px;
}
.navigation li a {
color: #444;
text-decoration: none;
display: block;
padding: 10px;
}
.navigation li a:hover {
background: rgba(153, 153, 153, 0.4);
border-radius: 5px;
}
.navigation li a span { margin-right: 10px; }
.navigation .create-shelf {
margin: 30px 0;
font-size: 12px;
text-align: center;
}
.navigation .create-shelf a {
color: #fff;
background: #45b29d;
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
}
.container-fluid img {
display: block;
max-width: 100%;
height: auto;
}
.container-fluid .discover{ margin-bottom: 50px; }
.container-fluid .new-books { border-top: 1px solid #ccc; }
.container-fluid .new-books h2 { margin: 50px 0 0 0; }
.container-fluid .book { margin-top: 20px; }
.container-fluid .book .cover {
height: 225px;
position: relative;
}
.container-fluid .book .cover img {
border: 1px solid #fff;
box-sizeing: border-box;
height: 100%;
bottom: 0;
position: absolute;
-webkit-box-shadow: 0 5px 8px -6px #777;
-moz-box-shadow: 0 5px 8px -6px #777;
box-shadow: 0 5px 8px -6px #777;
}
.container-fluid .book .meta { margin-top: 10px; }
.container-fluid .book .meta p { margin: 0; }
.container-fluid .book .meta .title {
font-weight: bold;
font-size: 15px;
color: #444;
}
.container-fluid .book .meta .author {
font-size: 12px;
color: #999;
}
.container-fluid .book .meta .rating { margin-top: 5px; }
.rating .glyphicon-star { color: #999; }
.rating .glyphicon-star.good { color: #45b29d; }
.container-fluid .author .author-hidden, .container-fluid .author .author-hidden-divider { display: none; }
.navbar-brand {
font-family: 'Grand Hotel', cursive;
font-size: 35px;
color: #45b29d !important;
}
.more-stuff {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ccc;
}
.more-stuff>li { margin-bottom: 10px; }
.navbar-collapse.in .navbar-nav { margin: 0; }
span.glyphicon.glyphicon-tags {
padding-right: 5px;
color: #999;
vertical-align: text-top;
}
.book-meta { padding-bottom: 20px; }
.book-meta .tags a { display: inline; }
.book-meta .identifiers a { display: inline; }
.container-fluid .single .cover img {
border: 1px solid #fff;
/*border-radius: 7px;*/
box-sizeing: border-box;
-webkit-box-shadow: 0 5px 8px -6px #777;
-moz-box-shadow: 0 5px 8px -6px #777;
box-shadow: 0 5px 8px -6px #777;
border: 1px solid #fff;
box-sizeing: border-box;
-webkit-box-shadow: 0 5px 8px -6px #777;
-moz-box-shadow: 0 5px 8px -6px #777;
box-shadow: 0 5px 8px -6px #777;
}
.navbar-default .navbar-toggle .icon-bar {background-color: #000;}
.navbar-default .navbar-toggle {border-color: #000;}
.cover { margin-bottom: 10px;}
.navbar-default .navbar-toggle .icon-bar {background-color: #000; }
.navbar-default .navbar-toggle {border-color: #000; }
.cover { margin-bottom: 10px; }
.cover-height { max-height: 100px;}
.col-sm-2 a .cover-small {
margin: 5px;
max-height: 200px;
}
.btn-file {position: relative; overflow: hidden;}
.btn-file input[type=file] {position: absolute; top: 0; right: 0; min-width: 100%; min-height: 100%; font-size: 100px; text-align: right; filter: alpha(opacity=0); opacity: 0; outline: none; background: white; cursor: inherit; display: block;}
.btn-file input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 100px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
outline: none;
background: white;
cursor: inherit;
display: block;
}
.btn-toolbar .btn,.discover .btn { margin-bottom: 5px; }
.button-link {color:#fff;}
.button-link {color: #fff; }
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; }
.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; }
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left:0px; }
.panel-body {background-color: #f5f5f5;}
.spinner {margin:0 41%;}
.spinner2 {margin:0 41%;}
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0px; }
.panel-body {background-color: #f5f5f5; }
.spinner {margin: 0 41%; }
.spinner2 {margin: 0 41%; }
table .bg-dark-danger {background-color: #d9534f; color: #fff; }
table .bg-dark-danger a {color: #fff; }
table .bg-dark-danger:hover {background-color: #c9302c; }
table .bg-primary:hover {background-color: #1C5484; }
table .bg-primary a {color: #fff; }
.block-label {display: block;}
.fake-input {position: absolute; pointer-events: none; top: 0;}
.fake-input {position: absolute; pointer-events: none; top: 0; }
input.pill { position: absolute; opacity: 0; }
input.pill + label {
border: 2px solid #45b29d;
border-radius: 15px;
color: #45b29d;
cursor: pointer;
display: inline-block;
padding: 3px 15px;
user-select: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
border: 2px solid #45b29d;
border-radius: 15px;
color: #45b29d;
cursor: pointer;
display: inline-block;
padding: 3px 15px;
user-select: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input.pill:checked + label {
background-color: #45b29d;
border-color: #fff;
color: #fff;
}
input.pill:not(:checked) + label .glyphicon {
display: none;
background-color: #45b29d;
border-color: #fff;
color: #fff;
}
.author-bio img {margin: 0 1em 1em 0;}
.author-link {display: inline-block; margin-top: 10px; width: 100px;}
.author-link img {display: block; height: 100%;}
input.pill:not(:checked) + label .glyphicon { display: none; }
#remove-from-shelves .btn,
#shelf-action-errors {
margin-left: 5px;
}
.author-bio img { margin: 0 1em 1em 0; }
.author-link { display: inline-block; margin-top: 10px; width: 100px; }
.author-link img { display: block; height: 100%; }
#remove-from-shelves .btn, #shelf-action-errors { margin-left: 5px; }
.tags_click, .serie_click, .language_click {margin-right: 5px;}
.tags_click, .serie_click, .language_click { margin-right: 5px; }
#meta-info {
height:600px;
overflow-y:scroll;
height: 600px;
overflow-y: scroll;
}
.media-list {
padding-right:15px;
.media-list { padding-right: 15px; }
.media-body p { text-align: justify; }
#meta-info img {
max-height: 150px;
max-width: 100px;
cursor: pointer;
}
.media-body p {
text-align: justify;
}
#meta-info img { max-height: 150px; max-width: 100px; cursor: pointer; }
.padded-bottom { margin-bottom: 15px; }
.upload-format-input-text { display: initial; }
#btn-upload-format { display: none; }
.upload-cover-input-text { display: initial; }
#btn-upload-cover { display: none; }
.panel-title > a { text-decoration: none; }
.editable-buttons {
display:inline-block;
margin-left: 7px;
}
.upload-format-input-text {display: initial;}
#btn-upload-format {display: none;}
.editable-input { display:inline-block; }
.upload-cover-input-text {display: initial;}
#btn-upload-cover {display: none;}
.panel-title > a { text-decoration: none;}
.editable-buttons { display:inline-block; margin-left: 7px ;}
.editable-input { display:inline-block;}
.editable-cancel { margin-bottom: 0px !important; margin-left: 7px !important;}
.editable-submit { margin-bottom: 0px !important;}
.editable-cancel {
margin-bottom: 0px !important;
margin-left: 7px !important;
}
.editable-submit { margin-bottom: 0px !important; }
.filterheader { margin-bottom: 20px; }
.errorlink { margin-top: 20px; }
.errorlink {margin-top: 20px;}
.modal-body .comments {
max-height:300px;
overflow-y: auto;
max-height: 300px;
overflow-y: auto;
}
div.log {
+6 -6
View File
@@ -1,8 +1,8 @@
@media (min-device-width: 768px) {
.upload-modal-dialog {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%) !important;
}
.upload-modal-dialog {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%) !important;
}
}
+4 -2
View File
@@ -40,7 +40,8 @@ function alphanumCase(a, b) {
while (i = (j = t.charAt(x++)).charCodeAt(0)) {
var m = (i === 46 || (i >= 48 && i <= 57));
if (m !== n) {
// Compare has to be with != otherwise fails
if (m != n) {
tz[++y] = "";
n = m;
}
@@ -55,7 +56,8 @@ function alphanumCase(a, b) {
for (var x = 0; aa[x] && bb[x]; x++) {
if (aa[x] !== bb[x]) {
var c = Number(aa[x]), d = Number(bb[x]);
if (c === aa[x] && d === bb[x]) {
// Compare has to be with == otherwise fails
if (c == aa[x] && d == bb[x]) {
return c - d;
} else {
return (aa[x] > bb[x]) ? 1 : -1;
+32 -6
View File
@@ -28,7 +28,7 @@ $( 'body.mailset' ).addClass( 'admin' );
curHref = window.location.href.split('/');
prevHref = document.referrer.split('/');
$( '.navbar-form.navbar-left' )
.before( '<div class="plexBack"><a href="' + document.referrer + '"></a></div>' );
.before( '<div class="plexBack"><a href="' + encodeURI(document.referrer) + '"></a></div>' );
if ( history.length === 1 ||
curHref[0] +
curHref[1] +
@@ -43,7 +43,7 @@ if ( history.length === 1 ||
//Weird missing a after pressing back from edit.
setTimeout(function() {
if ( $( '.plexBack a').length < 1 ) {
$( '.plexBack' ).append('<a href="' + document.referrer + '"></a>');
$( '.plexBack' ).append('<a href="' + encodeURI(document.referrer) + '"></a>');
}
},10);
@@ -159,10 +159,12 @@ if ( $( 'body.book' ).length > 0 ) {
real_custom_column = $( '.real_custom_columns' );
// $( '.real_custom_columns' ).remove();
$.each(real_custom_column, function(i, val) {
real_cc = $(this).text().split( ':' );
var split = $(this).text().split( ':' );
real_cc_key = split.shift();
real_cc_value = split.join(':');
$( this ).text("");
if (real_cc.length > 1) {
$( this ).append( '<span>' + real_cc[0] + '</span><span>' + real_cc[1] + '</span>' );
if (real_cc_value != "") {
$( this ).append( '<span>' + real_cc_key + '</span><span>' + real_cc_value + '</span>' );
}
});
//$( '.real_custom_columns:nth-child(3)' ).text(function() {
@@ -214,6 +216,8 @@ if ( $( 'body.book' ).length > 0 ) {
.prependTo( '[aria-label^="Download, send"]' );
$( '#have_read_cb' )
.after( '<label class="block-label readLbl" for="#have_read_cb"></label>' );
$( '#archived_cb' )
.after( '<label class="block-label readLbl" for="#archived_cb"></label>' );
$( '#shelf-actions' ).prependTo( '[aria-label^="Download, send"]' );
@@ -361,7 +365,7 @@ $( '#logout' ).parent().addClass( 'dropdown' ).appendTo( '.profileDropli' );
// Remove the modals except from some areas where they are needed
bodyClass = $( 'body' ).attr( 'class' ).split(' ');
modalWanted = ['admin', 'editbook', 'config', 'uiconfig'];
modalWanted = ['admin', 'editbook', 'config', 'uiconfig', 'me', 'edituser'];
if ( $.inArray( bodyClass[0], modalWanted) != -1 ) {
} else {
@@ -584,6 +588,20 @@ $( '#have_read_cb:checked' ).attr({
'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip');
$( '#archived_cb' ).attr({
'data-toggle': 'tooltip',
'title': $( '#archived_cb').attr('data-unchecked'),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip');
$( '#archived_cb:checked' ).attr({
'data-toggle': 'tooltip',
'title': $( '#archived_cb').attr('data-checked'),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip');
$( 'button#delete' ).attr({
'data-toggle-two': 'tooltip',
'title': $( 'button#delete' ).text(), //'Delete'
@@ -599,6 +617,14 @@ $( '#have_read_cb' ).click(function() {
}
});
$( '#archived_cb' ).click(function() {
if ( $( '#archived_cb:checked' ).length > 0 ) {
$( this ).attr('data-original-title', $('#archived_cb').attr('data-checked'));
} else {
$( this).attr('data-original-title', $('#archived_cb').attr('data-unchecked'));
}
});
$( '.btn-group[aria-label="Edit/Delete book"] a' ).attr({
'data-toggle': 'tooltip',
'title': $( '#edit_book' ).text(), // 'Edit'
+8
View File
@@ -25,6 +25,14 @@ $("#have_read_cb").on("change", function() {
$(this).closest("form").submit();
});
$(function() {
$("#archived_form").ajaxForm();
});
$("#archived_cb").on("change", function() {
$(this).closest("form").submit();
});
(function() {
var templates = {
add: _.template(
+3 -3
View File
@@ -45,7 +45,7 @@ $("#sort_name").click(function() {
});*/
// Find count of middle element
if (count > 20) {
var middle = parseInt(count / 2) + (count % 2);
var middle = parseInt(count / 2, 10) + (count % 2);
// search for the middle of all visibe elements
$(".row").each(function() {
index++;
@@ -146,7 +146,7 @@ $("#all").click(function() {
// Find count of middle element
var listItems = $("#list").children(".row");
var listlength = listItems.length;
var middle = parseInt(listlength / 2) + (listlength % 2);
var middle = parseInt(listlength / 2, 10) + (listlength % 2);
// go through all elements and make them visible
listItems.each(function() {
$(this).show();
@@ -178,7 +178,7 @@ $(".char").click(function() {
});
if (count > 20) {
// Find count of middle element
var middle = parseInt(count / 2) + (count % 2);
var middle = parseInt(count / 2, 10) + (count % 2);
// search for the middle of all visibe elements
$(".row").each(function() {
index++;
+186 -89
View File
@@ -15,23 +15,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Get Metadata from Douban Books api and Google Books api
* Get Metadata from Douban Books api and Google Books api and ComicVine
* Google Books api document: https://developers.google.com/books/docs/v1/using
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
* ComicVine api document: https://comicvine.gamespot.com/api/documentation
*/
/* global _, i18nMsg, tinymce */
/* global _, i18nMsg, tinymce */
var dbResults = [];
var ggResults = [];
var cvResults = [];
$(function () {
var msg = i18nMsg;
var douban = "https://api.douban.com";
var dbSearch = "/v2/book/search";
var dbDone = true;
var dbDone = 0;
var google = "https://www.googleapis.com";
var ggSearch = "/books/v1/volumes";
var ggDone = false;
var ggDone = 0;
var comicvine = "https://comicvine.gamespot.com";
var cvSearch = "/api/search/";
var cvDone = 0;
var showFlag = 0;
@@ -48,16 +54,17 @@ $(function () {
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
});
$("#bookAuthor").val(book.authors);
var ampSeparatedAuthors = (book.authors || []).join(" & ");
$("#bookAuthor").val(ampSeparatedAuthors);
$("#book_title").val(book.title);
$("#tags").val(uniqueTags.join(","));
$("#rating").data("rating").setValue(Math.round(book.rating));
$(".cover img").attr("src", book.cover);
$("#cover_url").val(book.cover);
$("#pubdate").val(book.publishedDate);
$("#publisher").val(book.publisher)
if (book.series != undefined) {
$("#series").val(book.series)
$("#publisher").val(book.publisher);
if (typeof book.series !== "undefined") {
$("#series").val(book.series);
}
}
@@ -66,99 +73,163 @@ $(function () {
if (showFlag === 1) {
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
}
if (!ggDone && !dbDone) {
if ((ggDone === 3 || (ggDone === 1 && ggResults.length === 0)) &&
(dbDone === 3 || (ggDone === 1 && dbResults.length === 0)) &&
(cvDone === 3 || (ggDone === 1 && cvResults.length === 0))) {
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
return;
}
function formatDate (date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
month = "" + (d.getMonth() + 1),
day = "" + d.getDate(),
year = d.getFullYear();
if (month.length < 2)
month = '0' + month;
if (day.length < 2)
day = '0' + day;
return [year, month, day].join('-');
if (month.length < 2) {
month = "0" + month;
}
if (day.length < 2) {
day = "0" + day;
}
return [year, month, day].join("-");
}
if (ggDone && ggResults.length > 0) {
ggResults.forEach(function(result) {
var book = {
id: result.id,
title: result.volumeInfo.title,
authors: result.volumeInfo.authors || [],
description: result.volumeInfo.description || "",
publisher: result.volumeInfo.publisher || "",
publishedDate: result.volumeInfo.publishedDate || "",
tags: result.volumeInfo.categories || [],
rating: result.volumeInfo.averageRating || 0,
cover: result.volumeInfo.imageLinks ?
result.volumeInfo.imageLinks.thumbnail : "/static/generic_cover.jpg",
url: "https://books.google.com/books?id=" + result.id,
source: {
id: "google",
description: "Google Books",
url: "https://books.google.com/"
}
};
if (ggResults.length > 0) {
if (ggDone < 2) {
ggResults.forEach(function(result) {
var book = {
id: result.id,
title: result.volumeInfo.title,
authors: result.volumeInfo.authors || [],
description: result.volumeInfo.description || "",
publisher: result.volumeInfo.publisher || "",
publishedDate: result.volumeInfo.publishedDate || "",
tags: result.volumeInfo.categories || [],
rating: result.volumeInfo.averageRating || 0,
cover: result.volumeInfo.imageLinks ?
result.volumeInfo.imageLinks.thumbnail : "/static/generic_cover.jpg",
url: "https://books.google.com/books?id=" + result.id,
source: {
id: "google",
description: "Google Books",
url: "https://books.google.com/"
}
};
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
$("#book-list").append($book);
});
ggDone = false;
ggDone = 2;
} else {
ggDone = 3;
}
}
if (dbDone && dbResults.length > 0) {
dbResults.forEach(function(result) {
if (result.series){
var series_title = result.series.title
}
var date_fomers = result.pubdate.split("-")
var publishedYear = parseInt(date_fomers[0])
var publishedMonth = parseInt(date_fomers[1])
var publishedDate = new Date(publishedYear, publishedMonth-1, 1)
publishedDate = formatDate(publishedDate)
var book = {
id: result.id,
title: result.title,
authors: result.author || [],
description: result.summary,
publisher: result.publisher || "",
publishedDate: publishedDate || "",
tags: result.tags.map(function(tag) {
return tag.title.toLowerCase().replace(/,/g, "_");
}),
rating: result.rating.average || 0,
series: series_title || "",
cover: result.image,
url: "https://book.douban.com/subject/" + result.id,
source: {
id: "douban",
description: "Douban Books",
url: "https://book.douban.com/"
if (dbResults.length > 0) {
if (dbDone < 2) {
dbResults.forEach(function(result) {
var seriesTitle = "";
if (result.series) {
seriesTitle = result.series.title;
}
};
var dateFomers = result.pubdate.split("-");
var publishedYear = parseInt(dateFomers[0]);
var publishedMonth = parseInt(dateFomers[1]);
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
if (book.rating > 0) {
book.rating /= 2;
}
publishedDate = formatDate(publishedDate);
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
var book = {
id: result.id,
title: result.title,
authors: result.author || [],
description: result.summary,
publisher: result.publisher || "",
publishedDate: publishedDate || "",
tags: result.tags.map(function(tag) {
return tag.title.toLowerCase().replace(/,/g, "_");
}),
rating: result.rating.average || 0,
series: seriesTitle || "",
cover: result.image,
url: "https://book.douban.com/subject/" + result.id,
source: {
id: "douban",
description: "Douban Books",
url: "https://book.douban.com/"
}
};
if (book.rating > 0) {
book.rating /= 2;
}
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
dbDone = 2;
} else {
dbDone = 3;
}
}
if (cvResults.length > 0) {
if (cvDone < 2) {
cvResults.forEach(function(result) {
var seriesTitle = "";
if (result.volume.name) {
seriesTitle = result.volume.name;
}
var dateFomers = "";
if (result.store_date) {
dateFomers = result.store_date.split("-");
} else {
dateFomers = result.date_added.split("-");
}
var publishedYear = parseInt(dateFomers[0]);
var publishedMonth = parseInt(dateFomers[1]);
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
$("#book-list").append($book);
});
dbDone = false;
publishedDate = formatDate(publishedDate);
var book = {
id: result.id,
title: seriesTitle + " #" + ("00" + result.issue_number).slice(-3) + " - " + result.name,
authors: result.author || [],
description: result.description,
publisher: "",
publishedDate: publishedDate || "",
tags: ["Comics", seriesTitle],
rating: 0,
series: seriesTitle || "",
cover: result.image.original_url,
url: result.site_detail_url,
source: {
id: "comicvine",
description: "ComicVine Books",
url: "https://comicvine.gamespot.com/"
}
};
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
cvDone = 2;
} else {
cvDone = 3;
}
}
}
@@ -171,11 +242,10 @@ $(function () {
success: function success(data) {
if ("items" in data) {
ggResults = data.items;
ggDone = true;
}
},
complete: function complete() {
ggDone = true;
ggDone = 1;
showResult();
$("#show-google").trigger("change");
}
@@ -183,7 +253,7 @@ $(function () {
}
function dbSearchBook (title) {
apikey="0df993c66c0c636e29ecbb5344252a4a"
var apikey = "0df993c66c0c636e29ecbb5344252a4a";
$.ajax({
url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10",
type: "GET",
@@ -193,22 +263,49 @@ $(function () {
dbResults = data.books;
},
error: function error() {
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>"+ $("#meta-info")[0].innerHTML)
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
},
complete: function complete() {
dbDone = true;
dbDone = 1;
showResult();
$("#show-douban").trigger("change");
}
});
}
function cvSearchBook (title) {
var apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6";
title = encodeURIComponent(title);
$.ajax({
url: comicvine + cvSearch + "?api_key=" + apikey + "&resources=issue&query=" + title + "&sort=name:desc&format=jsonp",
type: "GET",
dataType: "jsonp",
jsonp: "json_callback",
success: function success(data) {
cvResults = data.results;
},
error: function error() {
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
},
complete: function complete() {
cvDone = 1;
showResult();
$("#show-comics").trigger("change");
}
});
}
function doSearch (keyword) {
showFlag = 0;
dbDone = ggDone = cvDone = 0;
dbResults = [];
ggResults = [];
cvResults = [];
$("#meta-info").text(msg.loading);
if (keyword) {
dbSearchBook(keyword);
ggSearchBook(keyword);
cvSearchBook(keyword);
}
}
+2 -2
View File
File diff suppressed because one or more lines are too long
+3422 -2397
View File
File diff suppressed because it is too large Load Diff
+2895 -2066
View File
File diff suppressed because it is too large Load Diff
+936 -827
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -17,8 +17,7 @@
// Upon loading load the logfile for the first option (event log)
$(function() {
if ($("#log_group input").length)
{
if ($("#log_group input").length) {
var element = $("#log_group input[type='radio']:checked").val();
init(element);
}
+102 -18
View File
@@ -29,7 +29,6 @@ $(document).on("change", "input[type=\"checkbox\"][data-control]", function () {
});
});
// Generic control/related handler to show/hide fields based on a select' value
$(document).on("change", "select[data-control]", function() {
var $this = $(this);
@@ -39,13 +38,26 @@ $(document).on("change", "select[data-control]", function() {
for (var i = 0; i < $(this)[0].length; i++) {
var element = parseInt($(this)[0][i].value);
if (element === showOrHide) {
$("[data-related=" + name + "-" + element + "]").show();
$("[data-related^=" + name + "][data-related*=-" + element + "]").show();
} else {
$("[data-related=" + name + "-" + element + "]").hide();
$("[data-related^=" + name + "][data-related*=-" + element + "]").hide();
}
}
});
// Generic control/related handler to show/hide fields based on a select' value
// this one is made to show all values if select value is not 0
$(document).on("change", "select[data-controlall]", function() {
var $this = $(this);
var name = $this.data("controlall");
var showOrHide = parseInt($this.val());
if (showOrHide) {
$("[data-related=" + name + "]").show();
} else {
$("[data-related=" + name + "]").hide();
}
});
$(function() {
var updateTimerID;
@@ -61,29 +73,34 @@ $(function() {
$("#RestartDialog").modal("hide");
}
function cleanUp() {
clearInterval(updateTimerID);
$("#spinner2").hide();
$("#DialogFinished").removeClass("hidden");
$("#check_for_update").removeClass("hidden");
$("#perform_update").addClass("hidden");
$("#message").alert("close");
$("#update_table > tbody > tr").each(function () {
if ($(this).attr("id") !== "current_version") {
$(this).closest("tr").remove();
}
});
}
function updateTimer() {
$.ajax({
dataType: "json",
url: window.location.pathname + "/../../get_updater_status",
success: function success(data) {
// console.log(data.status);
$("#Updatecontent").html(updateText[data.status]);
$("#DialogContent").html(updateText[data.status]);
if (data.status > 6) {
clearInterval(updateTimerID);
$("#spinner2").hide();
$("#updateFinished").removeClass("hidden");
$("#check_for_update").removeClass("hidden");
$("#perform_update").addClass("hidden");
cleanUp();
}
},
error: function error() {
// console.log('Done');
clearInterval(updateTimerID);
$("#spinner2").hide();
$("#Updatecontent").html(updateText[7]);
$("#updateFinished").removeClass("hidden");
$("#check_for_update").removeClass("hidden");
$("#perform_update").addClass("hidden");
$("#DialogContent").html(updateText[7]);
cleanUp();
},
timeout: 2000
});
@@ -141,6 +158,8 @@ $(function() {
var $this = $(this);
var buttonText = $this.html();
$this.html("...");
$("#DialogContent").html("");
$("#DialogFinished").addClass("hidden");
$("#update_error").addClass("hidden");
if ($("#message").length) {
$("#message").alert("close");
@@ -182,13 +201,23 @@ $(function() {
});
});
$("#restart_database").click(function() {
$("#DialogHeader").addClass("hidden");
$("#DialogFinished").addClass("hidden");
$("#DialogContent").html("");
$("#spinner2").show();
$.ajax({
dataType: "json",
url: window.location.pathname + "/../../shutdown",
data: {"parameter":2}
data: {"parameter":2},
success: function success(data) {
$("#spinner2").hide();
$("#DialogContent").html(data.text);
$("#DialogFinished").removeClass("hidden");
}
});
});
$("#perform_update").click(function() {
$("#DialogHeader").removeClass("hidden");
$("#spinner2").show();
$.ajax({
type: "POST",
@@ -197,7 +226,7 @@ $(function() {
url: window.location.pathname + "/../../get_updater_status",
success: function success(data) {
updateText = data.text;
$("#Updatecontent").html(updateText[data.status]);
$("#DialogContent").html(updateText[data.status]);
// console.log(data.status);
updateTimerID = setInterval(updateTimer, 2000);
}
@@ -207,6 +236,7 @@ $(function() {
// Init all data control handlers to default
$("input[data-control]").trigger("change");
$("select[data-control]").trigger("change");
$("select[data-controlall]").trigger("change");
$("#bookDetailsModal")
.on("show.bs.modal", function(e) {
@@ -228,10 +258,64 @@ $(function() {
$(this).find(".modal-body").html("...");
});
$("#modal_kobo_token")
.on("show.bs.modal", function(e) {
var $modalBody = $(this).find(".modal-body");
// Prevent static assets from loading multiple times
var useCache = function(options) {
options.async = true;
options.cache = true;
};
preFilters.add(useCache);
$.get(e.relatedTarget.href).done(function(content) {
$modalBody.html(content);
preFilters.remove(useCache);
});
})
.on("hidden.bs.modal", function() {
$(this).find(".modal-body").html("...");
$("#config_delete_kobo_token").show();
});
$("#btndeletetoken").click(function() {
//get data-id attribute of the clicked element
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
// var domainId = $(this).value("domainId");
$.ajax({
method:"get",
url: path + "/../../kobo_auth/deleteauthtoken/" + this.value,
});
$("#modalDeleteToken").modal("hide");
$("#config_delete_kobo_token").hide();
});
$(window).resize(function() {
$(".discover .row").isotope("layout");
});
$("#import_ldap_users").click(function() {
$("#DialogHeader").addClass("hidden");
$("#DialogFinished").addClass("hidden");
$("#DialogContent").html("");
$("#spinner2").show();
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
$.ajax({
method:"get",
dataType: "json",
url: path + "/../../import_ldap_users",
success: function success(data) {
$("#spinner2").hide();
$("#DialogContent").html(data.text);
$("#DialogFinished").removeClass("hidden");
}
});
});
$(".author-expand").click(function() {
$(this).parent().find("a.author-name").slice($(this).data("authors-max")).toggle();
$(this).parent().find("span.author-hidden-divider").toggle();
+1 -1
View File
@@ -29,7 +29,7 @@ function sendData(path) {
var maxElements;
var tmp = [];
elements = Sortable.utils.find(sortTrue, "div");
elements = $(".list-group-item");
maxElements = elements.length;
var form = document.createElement("form");
+111 -2
View File
@@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* exported TableActions */
/* exported TableActions, RestrictionActions*/
$(function() {
@@ -93,10 +93,110 @@ $(function() {
var domainId = $(e.relatedTarget).data("domain-id");
$(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
});
$("#restrictModal").on("hidden.bs.modal", function () {
// Destroy table and remove hooks for buttons
$("#restrict-elements-table").unbind();
$("#restrict-elements-table").bootstrapTable("destroy");
$("[id^=submit_]").unbind();
$("#h1").addClass("hidden");
$("#h2").addClass("hidden");
$("#h3").addClass("hidden");
$("#h4").addClass("hidden");
});
function startTable(type) {
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
$("#restrict-elements-table").bootstrapTable({
formatNoMatches: function () {
return "";
},
url: path + "/../../ajax/listrestriction/" + type,
rowStyle: function(row) {
// console.log('Reihe :' + row + " Index :" + index);
if (row.id.charAt(0) === "a") {
return {classes: "bg-primary"};
} else {
return {classes: "bg-dark-danger"};
}
},
onClickCell: function (field, value, row) {
if (field === 3) {
$.ajax ({
type: "Post",
data: "id=" + row.id + "&type=" + row.type + "&Element=" + row.Element,
url: path + "/../../ajax/deleterestriction/" + type,
async: true,
timeout: 900,
success:function() {
$.ajax({
method:"get",
url: path + "/../../ajax/listrestriction/" + type,
async: true,
timeout: 900,
success:function(data) {
$("#restrict-elements-table").bootstrapTable("load", data);
}
});
}
});
}
},
striped: false
});
$("#restrict-elements-table").removeClass("table-hover");
$("#restrict-elements-table").on("editable-save.bs.table", function (e, field, row) {
$.ajax({
url: path + "/../../ajax/editrestriction/" + type,
type: "Post",
data: row
});
});
$("[id^=submit_]").click(function() {
$(this)[0].blur();
$.ajax({
url: path + "/../../ajax/addrestriction/" + type,
type: "Post",
data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
success: function () {
$.ajax ({
method:"get",
url: path + "/../../ajax/listrestriction/" + type,
async: true,
timeout: 900,
success:function(data) {
$("#restrict-elements-table").bootstrapTable("load", data);
}
});
}
});
return;
});
}
$("#get_column_values").on("click", function() {
startTable(1);
$("#h2").removeClass("hidden");
});
$("#get_tags").on("click", function() {
startTable(0);
$("#h1").removeClass("hidden");
});
$("#get_user_column_values").on("click", function() {
startTable(3);
$("#h4").removeClass("hidden");
});
$("#get_user_tags").on("click", function() {
startTable(2);
$(this)[0].blur();
$("#h3").removeClass("hidden");
});
});
/* Function for deleting domain restrictions */
function TableActions (value, row, index) {
function TableActions (value, row) {
return [
"<a class=\"danger remove\" data-toggle=\"modal\" data-target=\"#DeleteDomain\" data-domain-id=\"" + row.id
+ "\" title=\"Remove\">",
@@ -104,3 +204,12 @@ function TableActions (value, row, index) {
"</a>"
].join("");
}
/* Function for deleting domain restrictions */
function RestrictionActions (value, row) {
return [
"<div class=\"danger remove\" data-restriction-id=\"" + row.id + "\" title=\"Remove\">",
"<i class=\"glyphicon glyphicon-trash\"></i>",
"</div>"
].join("");
}
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=ملف PDF تالف أو غير صحيح.
missing_file_error=ملف PDF غير موجود.
unexpected_response_error=استجابة خادوم غير متوقعة.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}، {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Няспраўны або пашкоджаны файл PDF.
missing_file_error=Адсутны файл PDF.
unexpected_response_error=Нечаканы адказ сервера.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Restr PDF didalvoudek pe kontronet.
missing_file_error=Restr PDF o vankout.
unexpected_response_error=Respont dic'hortoz a-berzh an dafariad
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Man oke ta o yujtajinäq ri PDF yakb'äl.
missing_file_error=Man xilitäj ta ri PDF yakb'äl.
unexpected_response_error=Man oyob'en ta tz'olin rutzij ruk'u'x samaj.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Neplatný nebo chybný soubor PDF.
missing_file_error=Chybí soubor PDF.
unexpected_response_error=Neočekávaná odpověď serveru.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Ffeil PDF annilys neu llwgr.
missing_file_error=Ffeil PDF coll.
unexpected_response_error=Ymateb annisgwyl gan y gweinydd.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=PDF-filen er ugyldig eller ødelagt.
missing_file_error=Manglende PDF-fil.
unexpected_response_error=Uventet svar fra serveren.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Ungültige oder beschädigte PDF-Datei
missing_file_error=Fehlende PDF-Datei
unexpected_response_error=Unerwartete Antwort des Servers
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Μη έγκυρο ή κατεστραμμένο αρχείο
missing_file_error=Λείπει αρχείο PDF.
unexpected_response_error=Μη αναμενόμενη απόκριση από το διακομιστή.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -226,6 +226,10 @@ invalid_file_error=Invalid or corrupted PDF file.
missing_file_error=Missing PDF file.
unexpected_response_error=Unexpected server response.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -226,6 +226,10 @@ invalid_file_error=Invalid or corrupted PDF file.
missing_file_error=Missing PDF file.
unexpected_response_error=Unexpected server response.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -226,6 +226,10 @@ invalid_file_error=Invalid or corrupted PDF file.
missing_file_error=Missing PDF file.
unexpected_response_error=Unexpected server response.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Nevalida aŭ difektita PDF dosiero.
missing_file_error=Mankas dosiero PDF.
unexpected_response_error=Neatendita respondo de servilo.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -226,6 +226,10 @@ invalid_file_error=Archivo PDF no válido o cocrrupto.
missing_file_error=Archivo PDF faltante.
unexpected_response_error=Respuesta del servidor inesperada.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -226,6 +226,10 @@ invalid_file_error=Archivo PDF inválido o corrupto.
missing_file_error=Falta el archivo PDF.
unexpected_response_error=Respuesta del servidor inesperada.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -226,6 +226,10 @@ invalid_file_error=Fichero PDF no válido o corrupto.
missing_file_error=No hay fichero PDF.
unexpected_response_error=Respuesta inesperada del servidor.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Vigane või rikutud PDF-fail.
missing_file_error=PDF-fail puudub.
unexpected_response_error=Ootamatu vastus serverilt.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}} {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=PDF fitxategi baliogabe edo hondatua.
missing_file_error=PDF fitxategia falta da.
unexpected_response_error=Espero gabeko zerbitzariaren erantzuna.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Virheellinen tai vioittunut PDF-tiedosto.
missing_file_error=Puuttuva PDF-tiedosto.
unexpected_response_error=Odottamaton vastaus palvelimelta.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Fichier PDF invalide ou corrompu.
missing_file_error=Fichier PDF manquant.
unexpected_response_error=Réponse inattendue du serveur.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}} à {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -226,6 +226,10 @@ invalid_file_error=Ynfalide of korruptearre PDF-bestân.
missing_file_error=PDF-bestân ûntbrekt.
unexpected_response_error=Unferwacht serverantwurd.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=PDF marandurenda ndoikóiva térã ivaipyréva.
missing_file_error=Ndaipóri PDF marandurenda
unexpected_response_error=Mohendahavusu mbohovái ñeha'arõ'ỹva.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+7 -1
View File
@@ -173,6 +173,7 @@ find_reached_bottom=הגיע לסוף הדף, ממשיך מלמעלה
# "{{current}}" and "{{total}}" will be replaced by a number representing the
# index of the currently active find result, respectively a number representing
# the total number of matches in the document.
find_match_count={[ plural(total) ]}
find_match_count[one]=תוצאה {{current}} מתוך {{total}}
find_match_count[two]={{current}} מתוך {{total}} תוצאות
find_match_count[few]={{current}} מתוך {{total}} תוצאות
@@ -181,13 +182,14 @@ find_match_count[other]={{current}} מתוך {{total}} תוצאות
# LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are
# [zero|one|two|few|many|other], with [other] as the default value.
# "{{limit}}" will be replaced by a numerical value.
find_match_count_limit={[ plural(limit) ]}
find_match_count_limit[zero]=יותר מ־{{limit}} תוצאות
find_match_count_limit[one]=יותר מתוצאה אחת
find_match_count_limit[two]=יותר מ־{{limit}} תוצאות
find_match_count_limit[few]=יותר מ־{{limit}} תוצאות
find_match_count_limit[many]=יותר מ־{{limit}} תוצאות
find_match_count_limit[other]=יותר מ־{{limit}} תוצאות
find_not_found=ביטוי לא נמצא
find_not_found=הביטוי לא נמצא
# Error panel labels
error_more_info=מידע נוסף
@@ -224,6 +226,10 @@ invalid_file_error=קובץ PDF פגום או לא תקין.
missing_file_error=קובץ PDF חסר.
unexpected_response_error=תגובת שרת לא צפויה.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -208,6 +208,10 @@ invalid_file_error=अमान्य या भ्रष्ट PDF फ़ाइ
missing_file_error=\u0020अनुपस्थित PDF फ़ाइल.
unexpected_response_error=अप्रत्याशित सर्वर प्रतिक्रिया.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+38
View File
@@ -65,7 +65,19 @@ cursor_text_select_tool_label=Alat za označavanje teksta
cursor_hand_tool.title=Omogući ručni alat
cursor_hand_tool_label=Ručni alat
scroll_vertical.title=Koristi okomito pomicanje
scroll_vertical_label=Okomito pomicanje
scroll_horizontal.title=Koristi vodoravno pomicanje
scroll_horizontal_label=Vodoravno pomicanje
scroll_wrapped.title=Koristi omotano pomicanje
scroll_wrapped_label=Omotano pomicanje
spread_none.title=Ne pridružuj razmake stranica
spread_none_label=Bez razmaka
spread_odd.title=Pridruži razmake stranica počinjući od neparnih stranica
spread_odd_label=Neparni razmaci
spread_even.title=Pridruži razmake stranica počinjući od parnih stranica
spread_even_label=Parni razmaci
# Document properties dialog box
document_properties.title=Svojstva dokumenta...
@@ -91,8 +103,15 @@ document_properties_creator=Stvaratelj:
document_properties_producer=PDF stvaratelj:
document_properties_version=PDF inačica:
document_properties_page_count=Broj stranica:
document_properties_page_size=Dimenzije stranice:
document_properties_page_size_unit_inches=in
document_properties_page_size_unit_millimeters=mm
document_properties_page_size_orientation_portrait=portret
document_properties_page_size_orientation_landscape=pejzaž
document_properties_page_size_name_a3=A3
document_properties_page_size_name_a4=A4
document_properties_page_size_name_letter=Pismo
document_properties_page_size_name_legal=Pravno
# LOCALIZATION NOTE (document_properties_page_size_dimension_string):
# "{{width}}", "{{height}}", {{unit}}, and {{orientation}} will be replaced by
# the size, respectively their unit of measurement and orientation, of the (current) page.
@@ -103,6 +122,7 @@ document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}}
document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}})
# LOCALIZATION NOTE (document_properties_linearized): The linearization status of
# the document; usually called "Fast Web View" in English locales of Adobe software.
document_properties_linearized=Brzi web pregled:
document_properties_linearized_yes=Da
document_properties_linearized_no=Ne
document_properties_close=Zatvori
@@ -145,6 +165,7 @@ find_next.title=Pronađi iduće javljanje ovog izraza
find_next_label=Sljedeće
find_highlight=Istankni sve
find_match_case_label=Slučaj podudaranja
find_entire_word_label=Cijele riječi
find_reached_top=Dosegnut vrh dokumenta, nastavak od dna
find_reached_bottom=Dosegnut vrh dokumenta, nastavak od vrha
# LOCALIZATION NOTE (find_match_count): The supported plural forms are
@@ -152,9 +173,22 @@ find_reached_bottom=Dosegnut vrh dokumenta, nastavak od vrha
# "{{current}}" and "{{total}}" will be replaced by a number representing the
# index of the currently active find result, respectively a number representing
# the total number of matches in the document.
find_match_count={[ plural(total) ]}
find_match_count[one]={{current}} od {{total}} se podudara
find_match_count[two]={{current}} od {{total}} se podudara
find_match_count[few]={{current}} od {{total}} se podudara
find_match_count[many]={{current}} od {{total}} se podudara
find_match_count[other]={{current}} od {{total}} se podudara
# LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are
# [zero|one|two|few|many|other], with [other] as the default value.
# "{{limit}}" will be replaced by a numerical value.
find_match_count_limit={[ plural(limit) ]}
find_match_count_limit[zero]=Više od {{limit}} podudaranja
find_match_count_limit[one]=Više od {{limit}} podudaranja
find_match_count_limit[two]=Više od {{limit}} podudaranja
find_match_count_limit[few]=Više od {{limit}} podudaranja
find_match_count_limit[many]=Više od {{limit}} podudaranja
find_match_count_limit[other]=Više od {{limit}} podudaranja
find_not_found=Izraz nije pronađen
# Error panel labels
@@ -192,6 +226,10 @@ invalid_file_error=Kriva ili oštećena PDF datoteka.
missing_file_error=Nedostaje PDF datoteka.
unexpected_response_error=Neočekivani odgovor poslužitelja.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Njepłaćiwa abo wobškodźena PDF-dataja.
missing_file_error=Falowaca PDF-dataja.
unexpected_response_error=Njewočakowana serwerowa wotmołwa.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Érvénytelen vagy sérült PDF fájl.
missing_file_error=Hiányzó PDF fájl.
unexpected_response_error=Váratlan kiszolgálóválasz.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=File PDF corrumpite o non valide.
missing_file_error=File PDF mancante.
unexpected_response_error=Responsa del servitor inexpectate.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Berkas PDF tidak valid atau rusak.
missing_file_error=Berkas PDF tidak ada.
unexpected_response_error=Balasan server yang tidak diharapkan.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+21
View File
@@ -65,7 +65,17 @@ cursor_text_select_tool_label=Textavalsáhald
cursor_hand_tool.title=Virkja handarverkfæri
cursor_hand_tool_label=Handarverkfæri
scroll_vertical.title=Nota lóðrétt skrun
scroll_vertical_label=Lóðrétt skrun
scroll_horizontal.title=Nota lárétt skrun
scroll_horizontal_label=Lárétt skrun
spread_none.title=Ekki taka þátt í dreifingu síðna
spread_none_label=Engin dreifing
spread_odd.title=Taka þátt í dreifingu síðna með oddatölum
spread_odd_label=Oddatöludreifing
spread_even.title=Taktu þátt í dreifingu síðna með jöfnuntölum
spread_even_label=Jafnatöludreifing
# Document properties dialog box
document_properties.title=Eiginleikar skjals…
@@ -161,10 +171,21 @@ find_reached_bottom=Náði enda skjals, held áfram efst
# index of the currently active find result, respectively a number representing
# the total number of matches in the document.
find_match_count={[ plural(total) ]}
find_match_count[one]={{current}} af {{total}} niðurstöðu
find_match_count[two]={{current}} af {{total}} niðurstöðum
find_match_count[few]={{current}} af {{total}} niðurstöðum
find_match_count[many]={{current}} af {{total}} niðurstöðum
find_match_count[other]={{current}} af {{total}} niðurstöðum
# LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are
# [zero|one|two|few|many|other], with [other] as the default value.
# "{{limit}}" will be replaced by a numerical value.
find_match_count_limit={[ plural(limit) ]}
find_match_count_limit[zero]=Fleiri en {{limit}} niðurstöður
find_match_count_limit[one]=Fleiri en {{limit}} niðurstaða
find_match_count_limit[two]=Fleiri en {{limit}} niðurstöður
find_match_count_limit[few]=Fleiri en {{limit}} niðurstöður
find_match_count_limit[many]=Fleiri en {{limit}} niðurstöður
find_match_count_limit[other]=Fleiri en {{limit}} niðurstöður
find_not_found=Fann ekki orðið
# Error panel labels
+1
View File
@@ -146,6 +146,7 @@ loading_error = Si è verificato un errore durante il caricamento del PDF.
invalid_file_error = File PDF non valido o danneggiato.
missing_file_error = File PDF non disponibile.
unexpected_response_error = Risposta imprevista del server
annotation_date_string = {{date}}, {{time}}
text_annotation_type.alt = [Annotazione: {{type}}]
password_label = Inserire la password per aprire questo file PDF.
password_invalid = Password non corretta. Riprovare.
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=無効または破損した PDF ファイル。
missing_file_error=PDF ファイルが見つかりません。
unexpected_response_error=サーバーから予期せぬ応答がありました。
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+14 -10
View File
@@ -100,8 +100,8 @@ document_properties_modification_date=ჩასწორების თარ
# will be replaced by the creation/modification date, and time, of the PDF file.
document_properties_date_string={{date}}, {{time}}
document_properties_creator=გამომშვები:
document_properties_producer=PDF გამომშვები:
document_properties_version=PDF ვერსია:
document_properties_producer=PDF-გამომშვები:
document_properties_version=PDF-ვერსია:
document_properties_page_count=გვერდების რაოდენობა:
document_properties_page_size=გვერდის ზომა:
document_properties_page_size_unit_inches=დუიმი
@@ -122,7 +122,7 @@ document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}}
document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}})
# LOCALIZATION NOTE (document_properties_linearized): The linearization status of
# the document; usually called "Fast Web View" in English locales of Adobe software.
document_properties_linearized=Fast Web View:
document_properties_linearized=სწრაფი შეთვალიერება:
document_properties_linearized_yes=დიახ
document_properties_linearized_no=არა
document_properties_close=დახურვა
@@ -154,7 +154,7 @@ findbar_label=ძიება
thumb_page_title=გვერდი {{page}}
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
# number.
thumb_page_canvas=გვერდის ესკიზი {{page}}
thumb_page_canvas=გვერდის შეთვალიერება {{page}}
# Find panel button title and messages
find_input.title=ძიება
@@ -221,22 +221,26 @@ page_scale_percent={{scale}}%
# Loading indicator messages
loading_error_indicator=შეცდომა
loading_error=შეცდომა, PDF ფაილის ჩატვირთვისას.
invalid_file_error=არამართებული ან დაზიანებული PDF ფაილი.
missing_file_error=ნაკლული PDF ფაილი.
loading_error=შეცდომა, PDF-ფაილის ჩატვირთვისას.
invalid_file_error=არამართებული ან დაზიანებული PDF-ფაილი.
missing_file_error=ნაკლული PDF-ფაილი.
unexpected_response_error=სერვერის მოულოდნელი პასუხი.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
# Some common types are e.g.: "Check", "Text", "Comment", "Note"
text_annotation_type.alt=[{{type}} შენიშვნა]
password_label=შეიყვანეთ პაროლი PDF ფაილის გასახსნელად.
password_label=შეიყვანეთ პაროლი PDF-ფაილის გასახსნელად.
password_invalid=არასწორი პაროლი. გთხოვთ, სცადოთ ხელახლა.
password_ok=კარგი
password_cancel=გაუქმება
printing_not_supported=გაფრთხილება: ამობეჭდვა ამ ბრაუზერში არაა სრულად მხარდაჭერილი.
printing_not_ready=გაფრთხილება: PDF სრულად ჩატვირთული არაა, ამობეჭდვის დასაწყებად.
web_fonts_disabled=ვებშრიფტები გამორთულია: ჩაშენებული PDF შრიფტების გამოყენება ვერ ხერხდება.
document_colors_not_allowed=PDF დოკუმენტებს არ აქვს საკუთარი ფერების გამოყენების ნებართვა: ბრაუზერში გამორთულია “გვერდებისთვის საკუთარი ფერების გამოყენების უფლება”.
web_fonts_disabled=ვებშრიფტები გამორთულია: ჩაშენებული PDF-შრიფტების გამოყენება ვერ ხერხდება.
document_colors_not_allowed=PDF-დოკუმენტებს არ აქვს საკუთარი ფერების გამოყენების ნებართვა: ბრაუზერში გამორთულია “გვერდებისთვის საკუთარი ფერების გამოყენების უფლება”.
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Afaylu PDF arameɣtu neɣ yexṣeṛ.
missing_file_error=Ulac afaylu PDF.
unexpected_response_error=Aqeddac yerra-d yir tiririt ur nettwaṛǧi ara.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Зақымдалған немесе қате PDF файл.
missing_file_error=PDF файлы жоқ.
unexpected_response_error=Сервердің күтпеген жауабы.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+29 -25
View File
@@ -26,23 +26,23 @@ of_pages=전체 {{pagesCount}}
# LOCALIZATION NOTE (page_of_pages): "{{pageNumber}}" and "{{pagesCount}}"
# will be replaced by a number representing the currently visible page,
# respectively a number representing the total number of pages in the document.
page_of_pages=({{pagesCount}} 중 {{pageNumber}})
page_of_pages=({{pageNumber}} / {{pagesCount}})
zoom_out.title=축소
zoom_out_label=축소
zoom_in.title=확대
zoom_in_label=확대
zoom.title=크기
presentation_mode.title=발표 모드로 전환
presentation_mode_label=발표 모드
zoom.title=확대/축소
presentation_mode.title=프레젠테이션 모드로 전환
presentation_mode_label=프레젠테이션 모드
open_file.title=파일 열기
open_file_label=열기
print.title=인쇄
print_label=인쇄
download.title=다운로드
download_label=다운로드
bookmark.title=지금 보이는 그대로 (복사하거나 새 창에 열기)
bookmark_label=지금 보이는 그대로
bookmark.title=현재 뷰 (복사하거나 새 창에 열기)
bookmark_label=현재 뷰
# Secondary toolbar and context menu
tools.title=도구
@@ -83,7 +83,7 @@ spread_even_label=짝수 펼쳐짐
document_properties.title=문서 속성…
document_properties_label=문서 속성…
document_properties_file_name=파일 이름:
document_properties_file_size=파일 사이즈:
document_properties_file_size=파일 크기:
# LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}"
# will be replaced by the PDF file size in kilobytes, respectively in bytes.
document_properties_kb={{size_kb}} KB ({{size_b}}바이트)
@@ -91,18 +91,18 @@ document_properties_kb={{size_kb}} KB ({{size_b}}바이트)
# will be replaced by the PDF file size in megabytes, respectively in bytes.
document_properties_mb={{size_mb}} MB ({{size_b}}바이트)
document_properties_title=제목:
document_properties_author=저자:
document_properties_author=작성자:
document_properties_subject=주제:
document_properties_keywords=키워드:
document_properties_creation_date=생성일:
document_properties_modification_date=수정일:
document_properties_creation_date=작성 날짜:
document_properties_modification_date=수정 날짜:
# LOCALIZATION NOTE (document_properties_date_string): "{{date}}" and "{{time}}"
# will be replaced by the creation/modification date, and time, of the PDF file.
document_properties_date_string={{date}}, {{time}}
document_properties_creator=생성자:
document_properties_producer=PDF 생성기:
document_properties_creator=작성 프로그램:
document_properties_producer=PDF 변환 소프트웨어:
document_properties_version=PDF 버전:
document_properties_page_count=총 페이지:
document_properties_page_count=페이지 수:
document_properties_page_size=페이지 크기:
document_properties_page_size_unit_inches=in
document_properties_page_size_unit_millimeters=mm
@@ -127,7 +127,7 @@ document_properties_linearized_yes=예
document_properties_linearized_no=아니오
document_properties_close=닫기
print_progress_message=문서 출력 준비중…
print_progress_message=인쇄 문서 준비중…
# LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by
# a numerical per cent value.
print_progress_percent={{progress}}%
@@ -151,10 +151,10 @@ findbar_label=검색
# Thumbnails panel item (tooltip and alt text for images)
# LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page
# number.
thumb_page_title={{page}}쪽
thumb_page_title={{page}} 페이지
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
# number.
thumb_page_canvas={{page}}쪽 미리보기
thumb_page_canvas={{page}} 페이지 미리보기
# Find panel button title and messages
find_input.title=찾기
@@ -164,7 +164,7 @@ find_previous_label=이전
find_next.title=지정 문자열에 일치하는 다음 부분을 검색
find_next_label=다음
find_highlight=모두 강조 표시
find_match_case_label=대문자/소문자 구별
find_match_case_label=대/소문자 구분
find_entire_word_label=전체 단어
find_reached_top=문서 처음까지 검색하고 끝으로 돌아와 검색했습니다.
find_reached_bottom=문서 끝까지 검색하고 앞으로 돌아와 검색했습니다.
@@ -208,12 +208,12 @@ error_stack=스택: {{stack}}
error_file=파일: {{file}}
# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number
error_line=줄 번호: {{line}}
rendering_error=페이지를 렌더링하다 오류가 났습니다.
rendering_error=페이지를 렌더링하는 중 오류가 발생했습니다.
# Predefined zoom values
page_scale_width=페이지 너비에 맞춤
page_scale_fit=페이지에 맞춤
page_scale_auto=알아서 맞춤
page_scale_auto=자동 맞춤
page_scale_actual=실제 크기에 맞춤
# LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a
# numerical scale value.
@@ -221,22 +221,26 @@ page_scale_percent={{scale}}%
# Loading indicator messages
loading_error_indicator=오류
loading_error=PDF를 읽는 중 오류가 생겼습니다.
loading_error=PDF를 로드하는 중 오류가 발생했습니다.
invalid_file_error=유효하지 않거나 파손된 PDF 파일
missing_file_error=PDF 파일이 없습니다.
unexpected_response_error=알 수 없는 서버 응답입니다.
unexpected_response_error=예상치 못한 서버 응답입니다.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}} {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
# Some common types are e.g.: "Check", "Text", "Comment", "Note"
text_annotation_type.alt=[{{type}} 주석]
password_label=이 PDF 파일을 열 수 있는 암호를 입력하십시오.
password_invalid=잘못된 암호입니다. 다시 시도해 주십시오.
password_label=이 PDF 파일을 열 수 있는 비밀번호를 입력하십시오.
password_invalid=잘못된 비밀번호입니다. 다시 시도해 주십시오.
password_ok=확인
password_cancel=취소
printing_not_supported=경고: 이 브라우저는 인쇄를 완전히 지원하지 않습니다.
printing_not_ready=경고: 이 PDF를 인쇄를 할 수 있을 정도로 읽어들이지 못했습니다.
web_fonts_disabled=웹 폰트가 꺼져있음: 내장된 PDF 글꼴을 쓸 수 없습니다.
document_colors_not_allowed=PDF 문서의 색상을 쓰지 못하게 되어 있음: '웹 페이지 자체 색상 사용 허용'이 브라우저에서 꺼져 있습니다.
web_fonts_disabled=웹 폰트가 비활성화됨: 내장된 PDF 글꼴을 사용할 수 없습니다.
document_colors_not_allowed=PDF 문서의 자체 색상 허용 안됨: “페이지 자체 색상 허용”이 브라우저에서 비활성화 되어 있습니다.
+8 -8
View File
@@ -45,8 +45,8 @@ bookmark.title=Vixon corente (còpia ò arvi inte 'n neuvo barcon)
bookmark_label=Vixon corente
# Secondary toolbar and context menu
tools.title=Strumenti
tools_label=Strumenti
tools.title=Atressi
tools_label=Atressi
first_page.title=Vanni a-a primma pagina
first_page.label=Vanni a-a primma pagina
first_page_label=Vanni a-a primma pagina
@@ -82,8 +82,8 @@ spread_even_label=Difuxon pari
# Document properties dialog box
document_properties.title=Propietæ do documento…
document_properties_label=Propietæ do documento…
document_properties_file_name=Nomme file:
document_properties_file_size=Dimenscion file:
document_properties_file_name=Nomme schedaio:
document_properties_file_size=Dimenscion schedaio:
# LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}"
# will be replaced by the PDF file size in kilobytes, respectively in bytes.
document_properties_kb={{size_kb}} kB ({{size_b}} byte)
@@ -205,7 +205,7 @@ error_message=Mesaggio: {{message}}
# trace.
error_stack=Stack: {{stack}}
# LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename
error_file=File: {{file}}
error_file=Schedaio: {{file}}
# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number
error_line=Linia: {{line}}
rendering_error=Gh'é stæto 'n'erô itno rendering da pagina.
@@ -222,8 +222,8 @@ page_scale_percent={{scale}}%
# Loading indicator messages
loading_error_indicator=Erô
loading_error=S'é verificou 'n'erô itno caregamento do PDF.
invalid_file_error=O file PDF o l'é no valido ò aroinou.
missing_file_error=O file PDF o no gh'é.
invalid_file_error=O schedaio PDF o l'é no valido ò aroinou.
missing_file_error=O schedaio PDF o no gh'é.
unexpected_response_error=Risposta inprevista do-u server
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
@@ -231,7 +231,7 @@ unexpected_response_error=Risposta inprevista do-u server
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
# Some common types are e.g.: "Check", "Text", "Comment", "Note"
text_annotation_type.alt=[Anotaçion: {{type}}]
password_label=Dimme a paròlla segreta pe arvî sto file PDF.
password_label=Dimme a paròlla segreta pe arvî sto schedaio PDF.
password_invalid=Paròlla segreta sbalia. Preuva torna.
password_ok=Va ben
password_cancel=Anulla
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Tai nėra PDF failas arba jis yra sugadintas.
missing_file_error=PDF failas nerastas.
unexpected_response_error=Netikėtas serverio atsakas.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+21
View File
@@ -65,6 +65,10 @@ cursor_text_select_tool_label=मजकूर निवड साधन
cursor_hand_tool.title=हात साधन कार्यान्वित करा
cursor_hand_tool_label=हस्त साधन
scroll_vertical.title=अनुलंब स्क्रोलिंग वापरा
scroll_vertical_label=अनुलंब स्क्रोलिंग
scroll_horizontal.title=क्षैतिज स्क्रोलिंग वापरा
scroll_horizontal_label=क्षैतिज स्क्रोलिंग
# Document properties dialog box
@@ -95,6 +99,7 @@ document_properties_page_size=पृष्ठ आकार:
document_properties_page_size_unit_inches=इंच
document_properties_page_size_unit_millimeters=मीमी
document_properties_page_size_orientation_portrait=उभी मांडणी
document_properties_page_size_orientation_landscape=आडवे
document_properties_page_size_name_a3=A3
document_properties_page_size_name_a4=A4
document_properties_page_size_name_letter=Letter
@@ -109,6 +114,7 @@ document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}}
document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}})
# LOCALIZATION NOTE (document_properties_linearized): The linearization status of
# the document; usually called "Fast Web View" in English locales of Adobe software.
document_properties_linearized=जलद वेब दृष्य:
document_properties_linearized_yes=हो
document_properties_linearized_no=नाही
document_properties_close=बंद करा
@@ -151,8 +157,23 @@ find_next.title=वाकप्रयोगची पुढील घटना
find_next_label=पुढील
find_highlight=सर्व ठळक करा
find_match_case_label=आकार जुळवा
find_entire_word_label=संपूर्ण शब्द
find_reached_top=दस्तऐवजाच्या शीर्षकास पोहचले, तळपासून पुढे
find_reached_bottom=दस्तऐवजाच्या तळाला पोहचले, शीर्षकापासून पुढे
# LOCALIZATION NOTE (find_match_count): The supported plural forms are
# [one|two|few|many|other], with [other] as the default value.
# "{{current}}" and "{{total}}" will be replaced by a number representing the
# index of the currently active find result, respectively a number representing
# the total number of matches in the document.
find_match_count={[ plural(total) ]}
# LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are
# [zero|one|two|few|many|other], with [other] as the default value.
# "{{limit}}" will be replaced by a numerical value.
find_match_count_limit[zero]={{limit}} पेक्षा अधिक जुळण्या
find_match_count_limit[two]={{limit}} पेक्षा अधिक जुळण्या
find_match_count_limit[few]={{limit}} पेक्षा अधिक जुळण्या
find_match_count_limit[many]={{limit}} पेक्षा अधिक जुळण्या
find_match_count_limit[other]={{limit}} पेक्षा अधिक जुळण्या
find_not_found=वाकप्रयोग आढळले नाही
# Error panel labels
@@ -226,6 +226,10 @@ invalid_file_error=Ugyldig eller skadet PDF-fil.
missing_file_error=Manglende PDF-fil.
unexpected_response_error=Uventet serverrespons.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
+4
View File
@@ -226,6 +226,10 @@ invalid_file_error=Ongeldig of beschadigd PDF-bestand.
missing_file_error=PDF-bestand ontbreekt.
unexpected_response_error=Onverwacht serverantwoord.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
@@ -226,6 +226,10 @@ invalid_file_error=Ugyldig eller korrupt PDF-fil.
missing_file_error=Manglande PDF-fil.
unexpected_response_error=Uventa tenarrespons.
# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}} {{time}}
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).

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